pub fn extract_mr_document(conn: &Connection, mr_id: i64) -> Result> { let row = conn.query_row( "SELECT m.id, m.iid, m.title, m.description, m.state, m.author_username, m.source_branch, m.target_branch, m.created_at, m.updated_at, m.web_url, p.path_with_namespace, p.id AS project_id FROM merge_requests m JOIN projects p ON p.id = m.project_id WHERE m.id = ?1", rusqlite::params![mr_id], |row| { Ok(( row.get::<_, i64>(0)?, row.get::<_, i64>(1)?, row.get::<_, Option>(2)?, row.get::<_, Option>(3)?, row.get::<_, Option>(4)?, row.get::<_, Option>(5)?, row.get::<_, Option>(6)?, row.get::<_, Option>(7)?, row.get::<_, Option>(8)?, row.get::<_, Option>(9)?, row.get::<_, Option>(10)?, row.get::<_, String>(11)?, row.get::<_, i64>(12)?, )) }, ); let ( id, iid, title, description, state, author_username, source_branch, target_branch, created_at, updated_at, web_url, path_with_namespace, project_id, ) = match row { Ok(r) => r, Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None), Err(e) => return Err(e.into()), }; let mut label_stmt = conn.prepare_cached( "SELECT l.name FROM mr_labels ml JOIN labels l ON l.id = ml.label_id WHERE ml.merge_request_id = ?1 ORDER BY l.name", )?; let labels: Vec = label_stmt .query_map(rusqlite::params![id], |row| row.get(0))? .collect::, _>>()?; let labels_json = serde_json::to_string(&labels).unwrap_or_else(|_| "[]".to_string()); let display_title = title.as_deref().unwrap_or("(untitled)"); let display_state = state.as_deref().unwrap_or("unknown"); let mut content = format!( "[[MergeRequest]] !{}: {}\nProject: {}\n", iid, display_title, path_with_namespace ); if let Some(ref url) = web_url { let _ = writeln!(content, "URL: {}", url); } let _ = writeln!(content, "Labels: {}", labels_json); let _ = writeln!(content, "State: {}", display_state); if let Some(ref author) = author_username { let _ = writeln!(content, "Author: @{}", author); } if let (Some(src), Some(tgt)) = (&source_branch, &target_branch) { let _ = writeln!(content, "Source: {} -> {}", src, tgt); } if let Some(ref desc) = description { content.push_str("\n--- Description ---\n\n"); // Pre-truncate to avoid unbounded memory allocation for huge descriptions let pre_trunc = pre_truncate_description(desc, MAX_DOCUMENT_BYTES_HARD); if pre_trunc.was_truncated { warn!( iid, original_bytes = pre_trunc.original_bytes, "MR description truncated (oversized)" ); } content.push_str(&pre_trunc.content); } let labels_hash = compute_list_hash(&labels); let paths_hash = compute_list_hash(&[]); let hard_cap = truncate_hard_cap(&content); let content_hash = compute_content_hash(&hard_cap.content); Ok(Some(DocumentData { source_type: SourceType::MergeRequest, source_id: id, project_id, author_username, labels, paths: Vec::new(), labels_hash, paths_hash, created_at: created_at.unwrap_or(0), updated_at: updated_at.unwrap_or(0), url: web_url, title: Some(display_title.to_string()), content_text: hard_cap.content, content_hash, is_truncated: hard_cap.is_truncated, truncated_reason: hard_cap.reason.map(|r| r.as_str().to_string()), })) }