diff --git a/src/cli/commands/explain.rs b/src/cli/commands/explain.rs index 04172f2..3293bdb 100644 --- a/src/cli/commands/explain.rs +++ b/src/cli/commands/explain.rs @@ -823,7 +823,8 @@ fn fetch_related_entities( vec![] }; - // Outgoing references (excluding closes, shown above) + // Outgoing references (excluding closes, shown above). + // Filter out unresolved refs (NULL target_entity_iid) to avoid rusqlite type errors. let mut out_stmt = conn.prepare( "SELECT er.target_entity_type, er.target_entity_iid, er.reference_type, \ COALESCE(i.title, mr.title) as title \ @@ -831,7 +832,8 @@ fn fetch_related_entities( LEFT JOIN issues i ON er.target_entity_type = 'issue' AND i.id = er.target_entity_id \ LEFT JOIN merge_requests mr ON er.target_entity_type = 'merge_request' AND mr.id = er.target_entity_id \ WHERE er.source_entity_type = ?1 AND er.source_entity_id = ?2 \ - AND er.reference_type != 'closes'", + AND er.reference_type != 'closes' \ + AND er.target_entity_iid IS NOT NULL", )?; let outgoing: Vec = out_stmt @@ -845,7 +847,8 @@ fn fetch_related_entities( })? .collect::, _>>()?; - // Incoming references (excluding closes) + // Incoming references (excluding closes). + // COALESCE(i.iid, mr.iid) can be NULL if the source entity was deleted; filter those out. let mut in_stmt = conn.prepare( "SELECT er.source_entity_type, COALESCE(i.iid, mr.iid) as iid, er.reference_type, \ COALESCE(i.title, mr.title) as title \ @@ -853,7 +856,8 @@ fn fetch_related_entities( LEFT JOIN issues i ON er.source_entity_type = 'issue' AND i.id = er.source_entity_id \ LEFT JOIN merge_requests mr ON er.source_entity_type = 'merge_request' AND mr.id = er.source_entity_id \ WHERE er.target_entity_type = ?1 AND er.target_entity_id = ?2 \ - AND er.reference_type != 'closes'", + AND er.reference_type != 'closes' \ + AND COALESCE(i.iid, mr.iid) IS NOT NULL", )?; let incoming: Vec = in_stmt @@ -1924,6 +1928,29 @@ mod tests { assert_eq!(related.closing_mrs[0].state, "merged"); } + #[test] + fn test_explain_related_skips_unresolved_refs() { + let (conn, project_id) = setup_explain_db(); + let issue_id = insert_test_issue(&conn, project_id, 65, Some("desc")); + + // Insert an unresolved cross-project reference (NULL target_entity_iid) + conn.execute( + "INSERT INTO entity_references (project_id, source_entity_type, source_entity_id, \ + target_entity_type, target_entity_id, target_entity_iid, reference_type, \ + source_method, created_at) \ + VALUES (?, 'issue', ?, 'issue', NULL, NULL, 'mentioned', 'note_parse', 1000000)", + rusqlite::params![project_id, issue_id], + ) + .unwrap(); + + // Should NOT crash — unresolved refs are filtered out + let related = fetch_related_entities(&conn, "issues", issue_id).unwrap(); + assert!( + related.related_issues.is_empty(), + "Unresolved refs (NULL iid) should be excluded" + ); + } + #[test] fn test_explain_empty_activity() { let (conn, project_id) = setup_explain_db();