pub fn extract_note_document(conn: &Connection, note_id: i64) -> Result> { let row = conn.query_row( "SELECT n.id, n.gitlab_id, n.author_username, n.body, n.note_type, n.is_system, n.created_at, n.updated_at, n.position_new_path, n.position_new_line, n.position_old_path, n.position_old_line, n.resolvable, n.resolved, n.resolved_by, d.noteable_type, d.issue_id, d.merge_request_id, p.path_with_namespace, p.id AS project_id FROM notes n JOIN discussions d ON n.discussion_id = d.id JOIN projects p ON n.project_id = p.id WHERE n.id = ?1", rusqlite::params![note_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::<_, bool>(5)?, row.get::<_, i64>(6)?, row.get::<_, i64>(7)?, row.get::<_, Option>(8)?, row.get::<_, Option>(9)?, row.get::<_, Option>(10)?, row.get::<_, Option>(11)?, row.get::<_, bool>(12)?, row.get::<_, bool>(13)?, row.get::<_, Option>(14)?, row.get::<_, String>(15)?, row.get::<_, Option>(16)?, row.get::<_, Option>(17)?, row.get::<_, String>(18)?, row.get::<_, i64>(19)?, )) }, ); let ( _id, gitlab_id, author_username, body, note_type, is_system, created_at, updated_at, position_new_path, position_new_line, position_old_path, _position_old_line, resolvable, resolved, _resolved_by, noteable_type, issue_id, merge_request_id, path_with_namespace, project_id, ) = match row { Ok(r) => r, Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None), Err(e) => return Err(e.into()), }; if is_system { return Ok(None); } let (parent_iid, parent_title, parent_web_url, parent_type_label, labels) = match noteable_type.as_str() { "Issue" => { let parent_id = match issue_id { Some(pid) => pid, None => return Ok(None), }; let parent = conn.query_row( "SELECT i.iid, i.title, i.web_url FROM issues i WHERE i.id = ?1", rusqlite::params![parent_id], |row| { Ok(( row.get::<_, i64>(0)?, row.get::<_, Option>(1)?, row.get::<_, Option>(2)?, )) }, ); let (iid, title, web_url) = match parent { 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 issue_labels il JOIN labels l ON l.id = il.label_id WHERE il.issue_id = ?1 ORDER BY l.name", )?; let labels: Vec = label_stmt .query_map(rusqlite::params![parent_id], |row| row.get(0))? .collect::, _>>()?; (iid, title, web_url, "Issue", labels) } "MergeRequest" => { let parent_id = match merge_request_id { Some(pid) => pid, None => return Ok(None), }; let parent = conn.query_row( "SELECT m.iid, m.title, m.web_url FROM merge_requests m WHERE m.id = ?1", rusqlite::params![parent_id], |row| { Ok(( row.get::<_, i64>(0)?, row.get::<_, Option>(1)?, row.get::<_, Option>(2)?, )) }, ); let (iid, title, web_url) = match parent { 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![parent_id], |row| row.get(0))? .collect::, _>>()?; (iid, title, web_url, "MergeRequest", labels) } _ => return Ok(None), }; build_note_document( note_id, gitlab_id, author_username, body, note_type, created_at, updated_at, position_new_path, position_new_line, position_old_path, resolvable, resolved, parent_iid, parent_title.as_deref(), parent_web_url.as_deref(), &labels, parent_type_label, &path_with_namespace, project_id, ) } pub struct ParentMetadata { pub iid: i64, pub title: Option, pub web_url: Option, pub labels: Vec, pub project_path: String, } pub struct ParentMetadataCache { cache: HashMap<(String, i64), Option>, } impl Default for ParentMetadataCache { fn default() -> Self { Self::new() } } impl ParentMetadataCache { pub fn new() -> Self { Self { cache: HashMap::new(), } } pub fn get_or_fetch( &mut self, conn: &Connection, noteable_type: &str, parent_id: i64, project_path: &str, ) -> Result> { let key = (noteable_type.to_string(), parent_id); if !self.cache.contains_key(&key) { let meta = fetch_parent_metadata(conn, noteable_type, parent_id, project_path)?; self.cache.insert(key.clone(), meta); } Ok(self.cache.get(&key).and_then(|m| m.as_ref())) } } fn fetch_parent_metadata( conn: &Connection, noteable_type: &str, parent_id: i64, project_path: &str, ) -> Result> { match noteable_type { "Issue" => { let parent = conn.query_row( "SELECT i.iid, i.title, i.web_url FROM issues i WHERE i.id = ?1", rusqlite::params![parent_id], |row| { Ok(( row.get::<_, i64>(0)?, row.get::<_, Option>(1)?, row.get::<_, Option>(2)?, )) }, ); let (iid, title, web_url) = match parent { 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 issue_labels il JOIN labels l ON l.id = il.label_id WHERE il.issue_id = ?1 ORDER BY l.name", )?; let labels: Vec = label_stmt .query_map(rusqlite::params![parent_id], |row| row.get(0))? .collect::, _>>()?; Ok(Some(ParentMetadata { iid, title, web_url, labels, project_path: project_path.to_string(), })) } "MergeRequest" => { let parent = conn.query_row( "SELECT m.iid, m.title, m.web_url FROM merge_requests m WHERE m.id = ?1", rusqlite::params![parent_id], |row| { Ok(( row.get::<_, i64>(0)?, row.get::<_, Option>(1)?, row.get::<_, Option>(2)?, )) }, ); let (iid, title, web_url) = match parent { 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![parent_id], |row| row.get(0))? .collect::, _>>()?; Ok(Some(ParentMetadata { iid, title, web_url, labels, project_path: project_path.to_string(), })) } _ => Ok(None), } } pub fn extract_note_document_cached( conn: &Connection, note_id: i64, cache: &mut ParentMetadataCache, ) -> Result> { let row = conn.query_row( "SELECT n.id, n.gitlab_id, n.author_username, n.body, n.note_type, n.is_system, n.created_at, n.updated_at, n.position_new_path, n.position_new_line, n.position_old_path, n.position_old_line, n.resolvable, n.resolved, n.resolved_by, d.noteable_type, d.issue_id, d.merge_request_id, p.path_with_namespace, p.id AS project_id FROM notes n JOIN discussions d ON n.discussion_id = d.id JOIN projects p ON n.project_id = p.id WHERE n.id = ?1", rusqlite::params![note_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::<_, bool>(5)?, row.get::<_, i64>(6)?, row.get::<_, i64>(7)?, row.get::<_, Option>(8)?, row.get::<_, Option>(9)?, row.get::<_, Option>(10)?, row.get::<_, Option>(11)?, row.get::<_, bool>(12)?, row.get::<_, bool>(13)?, row.get::<_, Option>(14)?, row.get::<_, String>(15)?, row.get::<_, Option>(16)?, row.get::<_, Option>(17)?, row.get::<_, String>(18)?, row.get::<_, i64>(19)?, )) }, ); let ( _id, gitlab_id, author_username, body, note_type, is_system, created_at, updated_at, position_new_path, position_new_line, position_old_path, _position_old_line, resolvable, resolved, _resolved_by, noteable_type, issue_id, merge_request_id, path_with_namespace, project_id, ) = match row { Ok(r) => r, Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None), Err(e) => return Err(e.into()), }; if is_system { return Ok(None); } let parent_id = match noteable_type.as_str() { "Issue" => match issue_id { Some(pid) => pid, None => return Ok(None), }, "MergeRequest" => match merge_request_id { Some(pid) => pid, None => return Ok(None), }, _ => return Ok(None), }; let parent = cache.get_or_fetch(conn, ¬eable_type, parent_id, &path_with_namespace)?; let parent = match parent { Some(p) => p, None => return Ok(None), }; let parent_iid = parent.iid; let parent_title = parent.title.as_deref(); let parent_web_url = parent.web_url.as_deref(); let labels = parent.labels.clone(); let parent_type_label = noteable_type.as_str(); build_note_document( note_id, gitlab_id, author_username, body, note_type, created_at, updated_at, position_new_path, position_new_line, position_old_path, resolvable, resolved, parent_iid, parent_title, parent_web_url, &labels, parent_type_label, &path_with_namespace, project_id, ) } #[allow(clippy::too_many_arguments)] fn build_note_document( note_id: i64, gitlab_id: i64, author_username: Option, body: Option, note_type: Option, created_at: i64, updated_at: i64, position_new_path: Option, position_new_line: Option, position_old_path: Option, resolvable: bool, resolved: bool, parent_iid: i64, parent_title: Option<&str>, parent_web_url: Option<&str>, labels: &[String], parent_type_label: &str, path_with_namespace: &str, project_id: i64, ) -> Result> { let mut path_set = BTreeSet::new(); if let Some(ref p) = position_old_path && !p.is_empty() { path_set.insert(p.clone()); } if let Some(ref p) = position_new_path && !p.is_empty() { path_set.insert(p.clone()); } let paths: Vec = path_set.into_iter().collect(); let url = parent_web_url.map(|wu| format!("{}#note_{}", wu, gitlab_id)); let display_title = parent_title.unwrap_or("(untitled)"); let embed_title = normalize_title_for_embedding(display_title); let display_note_type = note_type.as_deref().unwrap_or("Note"); let display_author = author_username.as_deref().unwrap_or("unknown"); let parent_prefix = if parent_type_label == "Issue" { format!("Issue #{}", parent_iid) } else { format!("MR !{}", parent_iid) }; // Display title uses original (for human-readable output) let title = format!( "Note by @{} on {}: {}", display_author, parent_prefix, display_title ); let labels_csv = labels.join(", "); let mut content = String::new(); let _ = writeln!(content, "[[Note]]"); let _ = writeln!(content, "source_type: note"); let _ = writeln!(content, "note_gitlab_id: {}", gitlab_id); let _ = writeln!(content, "project: {}", path_with_namespace); let _ = writeln!(content, "parent_type: {}", parent_type_label); let _ = writeln!(content, "parent_iid: {}", parent_iid); let _ = writeln!(content, "parent_title: {}", embed_title); let _ = writeln!(content, "note_type: {}", display_note_type); let _ = writeln!(content, "author: @{}", display_author); let _ = writeln!(content, "created_at: {}", ms_to_iso(created_at)); if resolvable { let _ = writeln!(content, "resolved: {}", resolved); } if display_note_type == "DiffNote" && let Some(ref p) = position_new_path { if let Some(line) = position_new_line { let _ = writeln!(content, "path: {}:{}", p, line); } else { let _ = writeln!(content, "path: {}", p); } } if !labels.is_empty() { let _ = writeln!(content, "labels: {}", labels_csv); } if let Some(ref u) = url { let _ = writeln!(content, "url: {}", u); } content.push_str("\n--- Body ---\n\n"); content.push_str(body.as_deref().unwrap_or("")); let labels_hash = compute_list_hash(labels); let paths_hash = compute_list_hash(&paths); let hard_cap = truncate_hard_cap(&content); let content_hash = compute_content_hash(&hard_cap.content); Ok(Some(DocumentData { source_type: SourceType::Note, source_id: note_id, project_id, author_username, labels: labels.to_vec(), paths, labels_hash, paths_hash, created_at, updated_at, url, title: Some(title), 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()), })) }