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.
This commit is contained in:
teernisse
2026-03-10 15:54:54 -04:00
parent 32134ea933
commit 08bda08934

View File

@@ -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<RelatedEntityInfo> = out_stmt
@@ -845,7 +847,8 @@ fn fetch_related_entities(
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
// 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<RelatedEntityInfo> = 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();