use std::collections::HashSet;
use rusqlite::{params, Connection};
use tracing::debug;
use crate::core::errors::{Result, TgaError};
use crate::core::pm_work::{self, ExclusionReason, PmWorkVerdict};
const TRANSITIONS_SOURCE: &str = "jira";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PmWorkCandidate {
pub id: String,
pub source: String,
pub title: String,
pub raw_json: Option<String>,
pub human_transitioned: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PmWorkRow {
pub work_item_id: String,
pub work_item_source: String,
pub pm_name: Option<String>,
pub week_key: Option<String>,
pub verdict: PmWorkVerdict,
}
pub fn load_candidates(conn: &Connection) -> Result<Vec<PmWorkCandidate>> {
let human_moved = tickets_with_a_human_transition(conn)?;
let mut stmt = conn
.prepare("SELECT id, source, title, raw_json FROM work_items ORDER BY source, id")
.map_err(TgaError::from)?;
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, Option<String>>(3)?,
))
})
.map_err(TgaError::from)?;
let mut out = Vec::new();
for row in rows {
let (id, source, title, raw_json) = row.map_err(TgaError::from)?;
let human_transitioned = source == TRANSITIONS_SOURCE && human_moved.contains(&id);
out.push(PmWorkCandidate {
id,
source,
title,
raw_json,
human_transitioned,
});
}
Ok(out)
}
fn tickets_with_a_human_transition(conn: &Connection) -> Result<HashSet<String>> {
let mut stmt = conn
.prepare(
"SELECT DISTINCT ticket_key, author FROM fact_ticket_transitions \
WHERE author IS NOT NULL",
)
.map_err(TgaError::from)?;
let rows = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.map_err(TgaError::from)?;
let mut out = HashSet::new();
for row in rows {
let (ticket_key, author) = row.map_err(TgaError::from)?;
if !pm_work::is_bot_account(&author) {
out.insert(ticket_key);
}
}
Ok(out)
}
pub fn upsert_pm_work(conn: &Connection, row: &PmWorkRow) -> Result<()> {
let computed_at = chrono::Utc::now().timestamp();
conn.execute(
"INSERT OR REPLACE INTO fact_pm_work \
(work_item_id, work_item_source, pm_name, week_key, is_meaningful, \
exclusion_reason, title_word_count, body_word_count, formula_version, computed_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
params![
row.work_item_id,
row.work_item_source,
row.pm_name,
row.week_key,
i64::from(row.verdict.is_meaningful),
row.verdict.exclusion_reason.as_wire_str(),
row.verdict.title_word_count as i64,
row.verdict.body_word_count as i64,
pm_work::FORMULA_VERSION,
computed_at,
],
)
.map_err(TgaError::from)?;
debug!(
id = %row.work_item_id,
source = %row.work_item_source,
meaningful = row.verdict.is_meaningful,
reason = %row.verdict.exclusion_reason,
"upserted pm work verdict"
);
Ok(())
}
pub fn summarize(conn: &Connection) -> Result<(i64, Vec<(ExclusionReason, i64)>)> {
let mut stmt = conn
.prepare("SELECT exclusion_reason, COUNT(*) FROM fact_pm_work GROUP BY exclusion_reason")
.map_err(TgaError::from)?;
let rows = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
})
.map_err(TgaError::from)?;
let mut meaningful = 0;
let mut excluded: Vec<(ExclusionReason, i64)> = Vec::new();
for row in rows {
let (reason, count) = row.map_err(TgaError::from)?;
match ExclusionReason::from_wire_str(&reason) {
Some(ExclusionReason::None) => meaningful += count,
Some(other) => excluded.push((other, count)),
None => debug!(reason = %reason, "skipping unrecognised exclusion_reason"),
}
}
excluded.sort_by_key(|(reason, _)| reason.as_wire_str());
Ok((meaningful, excluded))
}