From 08bda08934759772dab235447c0b6bbc255f62d0 Mon Sep 17 00:00:00 2001 From: teernisse Date: Tue, 10 Mar 2026 15:54:54 -0400 Subject: [PATCH] fix(explain): filter out NULL iids in related entities queries entity_references.target_entity_iid is nullable (unresolved cross-project refs), and COALESCE(i.iid, mr.iid) returns NULL for orphaned refs. Both paths caused rusqlite InvalidColumnType errors when fetching i64. Added IS NOT NULL filters to both outgoing and incoming reference queries. --- src/cli/commands/explain.rs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) 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();