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,6 +1,6 @@
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::core::error::Result;
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::timeline::{EntityRef, ExpandedEntityRef, TimelineEvent, TimelineEventType};
|
||||
|
||||
/// Collect all events for seed and expanded entities, interleave chronologically.
|
||||
@@ -118,7 +118,7 @@ fn collect_state_events(
|
||||
is_seed: bool,
|
||||
events: &mut Vec<TimelineEvent>,
|
||||
) -> Result<()> {
|
||||
let (id_col, id_val) = entity_id_column(entity);
|
||||
let (id_col, id_val) = entity_id_column(entity)?;
|
||||
|
||||
let sql = format!(
|
||||
"SELECT state, actor_username, created_at FROM resource_state_events
|
||||
@@ -169,7 +169,7 @@ fn collect_label_events(
|
||||
is_seed: bool,
|
||||
events: &mut Vec<TimelineEvent>,
|
||||
) -> Result<()> {
|
||||
let (id_col, id_val) = entity_id_column(entity);
|
||||
let (id_col, id_val) = entity_id_column(entity)?;
|
||||
|
||||
let sql = format!(
|
||||
"SELECT action, label_name, actor_username, created_at FROM resource_label_events
|
||||
@@ -231,7 +231,7 @@ fn collect_milestone_events(
|
||||
is_seed: bool,
|
||||
events: &mut Vec<TimelineEvent>,
|
||||
) -> Result<()> {
|
||||
let (id_col, id_val) = entity_id_column(entity);
|
||||
let (id_col, id_val) = entity_id_column(entity)?;
|
||||
|
||||
let sql = format!(
|
||||
"SELECT action, milestone_title, actor_username, created_at FROM resource_milestone_events
|
||||
@@ -311,20 +311,25 @@ fn collect_merged_event(
|
||||
},
|
||||
);
|
||||
|
||||
if let Ok((Some(merged_at), merge_user, url)) = mr_result {
|
||||
events.push(TimelineEvent {
|
||||
timestamp: merged_at,
|
||||
entity_type: entity.entity_type.clone(),
|
||||
entity_id: entity.entity_id,
|
||||
entity_iid: entity.entity_iid,
|
||||
project_path: entity.project_path.clone(),
|
||||
event_type: TimelineEventType::Merged,
|
||||
summary: format!("MR !{} merged", entity.entity_iid),
|
||||
actor: merge_user,
|
||||
url,
|
||||
is_seed,
|
||||
});
|
||||
return Ok(());
|
||||
match mr_result {
|
||||
Ok((Some(merged_at), merge_user, url)) => {
|
||||
events.push(TimelineEvent {
|
||||
timestamp: merged_at,
|
||||
entity_type: entity.entity_type.clone(),
|
||||
entity_id: entity.entity_id,
|
||||
entity_iid: entity.entity_iid,
|
||||
project_path: entity.project_path.clone(),
|
||||
event_type: TimelineEventType::Merged,
|
||||
summary: format!("MR !{} merged", entity.entity_iid),
|
||||
actor: merge_user,
|
||||
url,
|
||||
is_seed,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
Ok((None, _, _)) => {} // merged_at is NULL, try fallback
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {} // entity not found, try fallback
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
// Fallback: check resource_state_events for state='merged'
|
||||
@@ -336,30 +341,37 @@ fn collect_merged_event(
|
||||
|row| Ok((row.get::<_, Option<String>>(0)?, row.get::<_, i64>(1)?)),
|
||||
);
|
||||
|
||||
if let Ok((actor, created_at)) = fallback_result {
|
||||
events.push(TimelineEvent {
|
||||
timestamp: created_at,
|
||||
entity_type: entity.entity_type.clone(),
|
||||
entity_id: entity.entity_id,
|
||||
entity_iid: entity.entity_iid,
|
||||
project_path: entity.project_path.clone(),
|
||||
event_type: TimelineEventType::Merged,
|
||||
summary: format!("MR !{} merged", entity.entity_iid),
|
||||
actor,
|
||||
url: None,
|
||||
is_seed,
|
||||
});
|
||||
match fallback_result {
|
||||
Ok((actor, created_at)) => {
|
||||
events.push(TimelineEvent {
|
||||
timestamp: created_at,
|
||||
entity_type: entity.entity_type.clone(),
|
||||
entity_id: entity.entity_id,
|
||||
entity_iid: entity.entity_iid,
|
||||
project_path: entity.project_path.clone(),
|
||||
event_type: TimelineEventType::Merged,
|
||||
summary: format!("MR !{} merged", entity.entity_iid),
|
||||
actor,
|
||||
url: None,
|
||||
is_seed,
|
||||
});
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {} // no merged state event, MR wasn't merged
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the correct column name and value for querying resource event tables.
|
||||
fn entity_id_column(entity: &EntityRef) -> (&'static str, i64) {
|
||||
fn entity_id_column(entity: &EntityRef) -> Result<(&'static str, i64)> {
|
||||
match entity.entity_type.as_str() {
|
||||
"issue" => ("issue_id", entity.entity_id),
|
||||
"merge_request" => ("merge_request_id", entity.entity_id),
|
||||
_ => ("issue_id", entity.entity_id), // shouldn't happen
|
||||
"issue" => Ok(("issue_id", entity.entity_id)),
|
||||
"merge_request" => Ok(("merge_request_id", entity.entity_id)),
|
||||
_ => Err(LoreError::Other(format!(
|
||||
"Unknown entity type for event collection: {}",
|
||||
entity.entity_type
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user