use anyhow::Result;
use serde::{Deserialize, Serialize};
use super::MetadataStore;
impl MetadataStore {
pub fn record_bead_lineage(&self, rec: &NewBeadLineage) -> Result<i64> {
let touched_files_json =
serde_json::to_string(&rec.touched_files).unwrap_or_else(|_| "[]".to_string());
let touched_symbols_json =
serde_json::to_string(&rec.touched_symbols).unwrap_or_else(|_| "[]".to_string());
self.conn.execute(
r#"INSERT INTO bead_lineage
(bead_id, bead_type, commit_sha, bundle_slugs, touched_files, action_type,
feature_id, lines_added, lines_deleted, touched_symbols)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"#,
rusqlite::params![
rec.bead_id,
rec.bead_type,
rec.commit_sha,
rec.bundle_slugs,
touched_files_json,
rec.action_type,
rec.feature_id,
rec.lines_added,
rec.lines_deleted,
touched_symbols_json,
],
)?;
Ok(self.conn.last_insert_rowid())
}
pub fn list_bead_lineage(
&self,
bead_id: Option<&str>,
commit_sha: Option<&str>,
limit: usize,
) -> Result<Vec<BeadLineageRecord>> {
let mut sql = String::from(
"SELECT id, created_at, bead_id, bead_type, commit_sha, bundle_slugs, touched_files, action_type,
feature_id, lines_added, lines_deleted, touched_symbols
FROM bead_lineage",
);
let mut conditions: Vec<String> = Vec::new();
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
if let Some(b) = bead_id {
conditions.push(format!("bead_id = ?{}", params.len() + 1));
params.push(Box::new(b.to_string()));
}
if let Some(c) = commit_sha {
conditions.push(format!("commit_sha = ?{}", params.len() + 1));
params.push(Box::new(c.to_string()));
}
if !conditions.is_empty() {
sql.push_str(" WHERE ");
sql.push_str(&conditions.join(" AND "));
}
sql.push_str(&format!(" ORDER BY id DESC LIMIT ?{}", params.len() + 1));
params.push(Box::new(limit as i64));
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
let mut stmt = self.conn.prepare(&sql)?;
let rows = stmt
.query_map(param_refs.as_slice(), |row| {
let touched_json: Option<String> = row.get(6)?;
let touched_files = touched_json
.and_then(|j| serde_json::from_str::<Vec<String>>(&j).ok())
.unwrap_or_default();
let symbols_json: Option<String> = row.get(11)?;
let touched_symbols = symbols_json
.and_then(|j| serde_json::from_str::<Vec<TouchedSymbol>>(&j).ok())
.unwrap_or_default();
Ok(BeadLineageRecord {
id: row.get(0)?,
created_at: row.get(1)?,
bead_id: row.get(2)?,
bead_type: row.get(3)?,
commit_sha: row.get(4)?,
bundle_slugs: row.get(5)?,
touched_files,
action_type: row.get(7)?,
feature_id: row.get(8)?,
lines_added: row.get(9)?,
lines_deleted: row.get(10)?,
touched_symbols,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn distinct_lineage_bead_ids(&self) -> Result<Vec<(String, Option<String>)>> {
let mut stmt = self.conn.prepare(
"SELECT bead_id, MAX(bead_type) FROM bead_lineage GROUP BY bead_id ORDER BY bead_id",
)?;
let rows = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn prior_lineage_touching_files(
&self,
files: &[String],
before: &str,
) -> Result<Vec<PriorTouch>> {
if files.is_empty() {
return Ok(Vec::new());
}
let placeholders = (0..files.len())
.map(|i| format!("?{}", i + 1))
.collect::<Vec<_>>()
.join(",");
let before_idx = files.len() + 1;
let sql = format!(
"SELECT bl.bead_id, bl.commit_sha, je.value, bl.created_at
FROM bead_lineage bl, json_each(bl.touched_files) je
WHERE bl.commit_sha IS NOT NULL
AND bl.created_at < ?{before_idx}
AND je.value IN ({placeholders})
ORDER BY bl.created_at DESC, bl.id DESC"
);
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
for f in files {
params.push(Box::new(f.clone()));
}
params.push(Box::new(before.to_string()));
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
let mut stmt = self.conn.prepare(&sql)?;
let rows = stmt
.query_map(param_refs.as_slice(), |row| {
Ok(PriorTouch {
bead_id: row.get(0)?,
commit_sha: row.get(1)?,
file: row.get(2)?,
created_at: row.get(3)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn record_bug_causality(&self, rec: &NewBugCausality) -> Result<i64> {
self.conn.execute(
r#"INSERT INTO bug_causality (bug_id, culprit_sha, culprit_bead_id, file, confidence)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(bug_id, culprit_sha, file) DO UPDATE SET
culprit_bead_id = excluded.culprit_bead_id,
confidence = excluded.confidence,
created_at = strftime('%Y-%m-%dT%H:%M:%SZ','now')"#,
rusqlite::params![
rec.bug_id,
rec.culprit_sha,
rec.culprit_bead_id,
rec.file,
rec.confidence,
],
)?;
Ok(self.conn.last_insert_rowid())
}
pub fn list_bug_causality(
&self,
bug_id: Option<&str>,
limit: usize,
) -> Result<Vec<BugCausalityRecord>> {
let mut sql = String::from(
"SELECT id, created_at, bug_id, culprit_sha, culprit_bead_id, file, confidence
FROM bug_causality",
);
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
if let Some(b) = bug_id {
sql.push_str(" WHERE bug_id = ?1");
params.push(Box::new(b.to_string()));
}
sql.push_str(&format!(
" ORDER BY confidence DESC, id DESC LIMIT ?{}",
params.len() + 1
));
params.push(Box::new(limit as i64));
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
let mut stmt = self.conn.prepare(&sql)?;
let rows = stmt
.query_map(param_refs.as_slice(), |row| {
Ok(BugCausalityRecord {
id: row.get(0)?,
created_at: row.get(1)?,
bug_id: row.get(2)?,
culprit_sha: row.get(3)?,
culprit_bead_id: row.get(4)?,
file: row.get(5)?,
confidence: row.get(6)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PriorTouch {
pub bead_id: String,
pub commit_sha: Option<String>,
pub file: String,
pub created_at: String,
}
#[derive(Debug, Clone, Default)]
pub struct NewBugCausality {
pub bug_id: String,
pub culprit_sha: Option<String>,
pub culprit_bead_id: Option<String>,
pub file: Option<String>,
pub confidence: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct BugCausalityRecord {
pub id: i64,
pub created_at: String,
pub bug_id: String,
pub culprit_sha: Option<String>,
pub culprit_bead_id: Option<String>,
pub file: Option<String>,
pub confidence: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TouchedSymbol {
pub file: String,
pub symbol: String,
pub kind: String,
}
#[derive(Debug, Clone, Default)]
pub struct NewBeadLineage {
pub bead_id: String,
pub bead_type: Option<String>,
pub commit_sha: Option<String>,
pub bundle_slugs: Option<String>,
pub touched_files: Vec<String>,
pub action_type: Option<String>,
pub feature_id: Option<String>,
pub lines_added: Option<i64>,
pub lines_deleted: Option<i64>,
pub touched_symbols: Vec<TouchedSymbol>,
}
#[derive(Debug, Clone)]
pub struct BeadLineageRecord {
pub id: i64,
pub created_at: String,
pub bead_id: String,
pub bead_type: Option<String>,
pub commit_sha: Option<String>,
pub bundle_slugs: Option<String>,
pub touched_files: Vec<String>,
pub action_type: Option<String>,
pub feature_id: Option<String>,
pub lines_added: Option<i64>,
pub lines_deleted: Option<i64>,
pub touched_symbols: Vec<TouchedSymbol>,
}