refactor(timeline): harden pipeline stages with shared resolver and exhaustive error handling
Follows up on the resolve_entity_ref extraction by updating all three pipeline stages to consume the shared helper and removing their local duplicates (~75 lines of dead code eliminated). timeline_seed.rs: - Switch from local resolve_entity to shared resolve_entity_ref with explicit Some(proj_id) scoping - Add tracing::debug for orphaned discussion parents instead of silently skipping them, aiding debugging when evidence notes go missing - Use saturating_mul for the over-fetch multiplier to prevent overflow on pathological max_seeds values timeline_expand.rs: - Switch from local resolve_entity_ref to shared version with None project scoping (cross-project traversal) - Pass Option<i64> for target_iid in UnresolvedRef construction instead of unwrap_or(0) sentinel - Update test assertion to compare against Some(42) timeline_collect.rs: - Make entity_id_column return Result instead of silently defaulting to issue_id for unknown entity types. The previous fallback could produce incorrect SQL queries that return wrong results rather than failing - Replace if-let chains in collect_merged_event with exhaustive match blocks that propagate real DB errors while gracefully handling expected missing-data cases (QueryReturnedNoRows, NULL merged_at) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use rusqlite::Connection;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::core::error::Result;
|
||||
use crate::core::timeline::{EntityRef, TimelineEvent, TimelineEventType};
|
||||
use crate::core::timeline::{EntityRef, TimelineEvent, TimelineEventType, resolve_entity_ref};
|
||||
use crate::search::{FtsQueryMode, to_fts_query};
|
||||
|
||||
/// Result of the seed + hydrate phases.
|
||||
@@ -67,7 +68,12 @@ fn find_seed_entities(
|
||||
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let rows = stmt.query_map(
|
||||
rusqlite::params![fts_query, project_id, since_ms, (max_seeds * 3) as i64],
|
||||
rusqlite::params![
|
||||
fts_query,
|
||||
project_id,
|
||||
since_ms,
|
||||
max_seeds.saturating_mul(3) as i64
|
||||
],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
@@ -105,7 +111,8 @@ fn find_seed_entities(
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(entity_ref) = resolve_entity(conn, &entity_type, entity_id, proj_id)? {
|
||||
if let Some(entity_ref) = resolve_entity_ref(conn, &entity_type, entity_id, Some(proj_id))?
|
||||
{
|
||||
entities.push(entity_ref);
|
||||
}
|
||||
|
||||
@@ -117,42 +124,6 @@ fn find_seed_entities(
|
||||
Ok(entities)
|
||||
}
|
||||
|
||||
/// Resolve an entity ID to a full EntityRef with iid and project_path.
|
||||
fn resolve_entity(
|
||||
conn: &Connection,
|
||||
entity_type: &str,
|
||||
entity_id: i64,
|
||||
project_id: i64,
|
||||
) -> Result<Option<EntityRef>> {
|
||||
let (table, id_col) = match entity_type {
|
||||
"issue" => ("issues", "id"),
|
||||
"merge_request" => ("merge_requests", "id"),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let sql = format!(
|
||||
"SELECT e.iid, p.path_with_namespace
|
||||
FROM {table} e
|
||||
JOIN projects p ON p.id = e.project_id
|
||||
WHERE e.{id_col} = ?1 AND e.project_id = ?2"
|
||||
);
|
||||
|
||||
let result = conn.query_row(&sql, rusqlite::params![entity_id, project_id], |row| {
|
||||
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok((iid, project_path)) => Ok(Some(EntityRef {
|
||||
entity_type: entity_type.to_owned(),
|
||||
entity_id,
|
||||
entity_iid: iid,
|
||||
project_path,
|
||||
})),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find evidence notes: FTS5-matched discussion notes that provide context.
|
||||
fn find_evidence_notes(
|
||||
conn: &Connection,
|
||||
@@ -211,10 +182,18 @@ fn find_evidence_notes(
|
||||
|
||||
let snippet = truncate_to_chars(body.as_deref().unwrap_or(""), 200);
|
||||
|
||||
let entity_ref = resolve_entity(conn, &parent_type, parent_entity_id, proj_id)?;
|
||||
let entity_ref = resolve_entity_ref(conn, &parent_type, parent_entity_id, Some(proj_id))?;
|
||||
let (iid, project_path) = match entity_ref {
|
||||
Some(ref e) => (e.entity_iid, e.project_path.clone()),
|
||||
None => continue,
|
||||
None => {
|
||||
debug!(
|
||||
parent_type,
|
||||
parent_entity_id,
|
||||
proj_id,
|
||||
"Skipping evidence note: parent entity not found (orphaned discussion)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
events.push(TimelineEvent {
|
||||
|
||||
Reference in New Issue
Block a user