use anyhow::Result;
use parking_lot::Mutex;
use rusqlite::{params, OptionalExtension};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct SymbolFeatures {
pub symbol_id: i64,
pub stable_id: String,
pub snapshot_id: i64,
pub loc: i64,
pub fan_in: i64,
pub fan_out: i64,
pub complexity: i64,
pub cfg_block_count: i64,
pub cfg_edge_count: i64,
pub conditional_density: f64,
pub lifetime: i64,
pub churn_count: i64,
}
impl SymbolFeatures {
pub fn get(&self, name: &str) -> Option<f64> {
match name {
"loc" => Some(self.loc as f64),
"fan_in" => Some(self.fan_in as f64),
"fan_out" => Some(self.fan_out as f64),
"complexity" => Some(self.complexity as f64),
"cfg_block_count" => Some(self.cfg_block_count as f64),
"cfg_edge_count" => Some(self.cfg_edge_count as f64),
"conditional_density" => Some(self.conditional_density),
"lifetime" => Some(self.lifetime as f64),
"churn_count" => Some(self.churn_count as f64),
_ => None,
}
}
}
pub struct FeatureExtractor {
db: Arc<Mutex<rusqlite::Connection>>,
}
impl FeatureExtractor {
pub fn new(db: Arc<Mutex<rusqlite::Connection>>) -> Self {
Self { db }
}
pub fn extract_for_symbol(&self, symbol_id: i64) -> Result<SymbolFeatures> {
let conn = self.db.lock();
let (loc, fan_in, fan_out, complexity, stable_id) = conn
.query_row(
"SELECT loc, fan_in, fan_out, cyclomatic_complexity, symbol_name
FROM symbol_metrics
WHERE symbol_id = ?1",
params![symbol_id],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get::<_, String>(4)?,
))
},
)
.optional()?
.ok_or_else(|| anyhow::anyhow!("Symbol {} not found in symbol_metrics", symbol_id))?;
let cfg_block_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_blocks WHERE function_id = ?1",
params![symbol_id],
|row| row.get(0),
)
.unwrap_or(0);
let cfg_edge_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_edges WHERE function_id = ?1",
params![symbol_id],
|row| row.get(0),
)
.unwrap_or(0);
let conditional_count: i64 = if cfg_block_count > 0 {
conn.query_row(
"SELECT COUNT(*) FROM cfg_blocks
WHERE function_id = ?1
AND kind IN ('if', 'match', 'while', 'for', 'loop')",
params![symbol_id],
|row| row.get(0),
)
.unwrap_or(0)
} else {
0
};
let conditional_density = if cfg_block_count > 0 {
conditional_count as f64 / cfg_block_count as f64
} else {
0.0
};
let (snapshot_id, lifetime, churn_count) = conn
.query_row(
"SELECT MAX(snapshot_id), COUNT(DISTINCT snapshot_id), COUNT(*)
FROM symbol_versions
WHERE name = ?1
GROUP BY name",
params![stable_id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap_or((0, 0, 0));
let snapshot_id = if snapshot_id == 0 {
conn.query_row("SELECT MAX(id) FROM repo_snapshots", [], |row| row.get(0))
.unwrap_or(0)
} else {
snapshot_id
};
drop(conn);
Ok(SymbolFeatures {
symbol_id,
stable_id,
snapshot_id,
loc,
fan_in,
fan_out,
complexity,
cfg_block_count,
cfg_edge_count,
conditional_density,
lifetime,
churn_count,
})
}
pub fn extract_batch(&self, symbol_ids: &[i64]) -> Result<Vec<SymbolFeatures>> {
let mut results = Vec::with_capacity(symbol_ids.len());
for &symbol_id in symbol_ids {
match self.extract_for_symbol(symbol_id) {
Ok(features) => results.push(features),
Err(e) => {
eprintln!(
"Warning: Failed to extract features for symbol {}: {}",
symbol_id, e
);
}
}
}
Ok(results)
}
pub fn extract_all(&self) -> Result<Vec<SymbolFeatures>> {
let conn = self.db.lock();
let symbol_ids: Vec<i64> = conn
.prepare(
"SELECT sm.symbol_id FROM symbol_metrics sm
INNER JOIN graph_entities ge ON sm.symbol_id = ge.id",
)?
.query_map([], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
drop(conn);
self.extract_batch(&symbol_ids)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symbol_features_get() {
let features = SymbolFeatures {
symbol_id: 1,
stable_id: "test::symbol".to_string(),
snapshot_id: 100,
loc: 100,
fan_in: 5,
fan_out: 3,
complexity: 8,
cfg_block_count: 10,
cfg_edge_count: 15,
conditional_density: 0.4,
lifetime: 50,
churn_count: 3,
};
assert_eq!(features.get("loc"), Some(100.0));
assert_eq!(features.get("fan_in"), Some(5.0));
assert_eq!(features.get("conditional_density"), Some(0.4));
assert_eq!(features.get("unknown"), None);
}
}