db.rs (5266B)
1 use rusqlite::{Connection, OpenFlags}; 2 use std::collections::HashMap; 3 4 #[derive(Debug, Clone)] 5 pub struct RawNode { 6 pub id: String, 7 pub title: String, 8 pub file: String, 9 pub level: i64, 10 pub pos: i64, 11 pub mtime: i64, 12 pub aliases: Vec<String>, 13 pub tags: Vec<String>, 14 } 15 16 pub fn load_graph(db_path: &str) -> rusqlite::Result<(Vec<RawNode>, Vec<(String, String)>)> { 17 let conn = Connection::open_with_flags( 18 db_path, 19 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI, 20 )?; 21 22 let mut nodes: HashMap<String, RawNode> = HashMap::new(); 23 24 // Try with files JOIN first; mtime may be Emacs `(HIGH LOW ...)` string 25 let rows_result = conn.prepare( 26 "SELECT n.id, n.title, n.file, n.level, n.pos, f.mtime 27 FROM nodes n LEFT JOIN files f ON n.file = f.file", 28 ); 29 30 match rows_result { 31 Ok(mut stmt) => { 32 let iter = stmt.query_map([], |row| { 33 Ok(( 34 row.get::<_, String>(0)?, 35 row.get::<_, Option<String>>(1)?.unwrap_or_default(), 36 row.get::<_, Option<String>>(2)?.unwrap_or_default(), 37 row.get::<_, Option<i64>>(3)?.unwrap_or(0), 38 row.get::<_, Option<i64>>(4)?.unwrap_or(0), 39 row.get::<_, Option<String>>(5)?, // may be "(HIGH LOW ...)" or integer 40 )) 41 })?; 42 for row in iter.flatten() { 43 let (id, title, file, level, pos, mtime_raw) = row; 44 let title = title.trim_matches('"').to_string(); 45 let file = file.trim_matches('"').to_string(); 46 let mtime = parse_mtime(mtime_raw.as_deref()); 47 nodes.insert(id.clone(), 48 RawNode { id, title, file, level, pos, mtime, aliases: vec![], tags: vec![] }); 49 } 50 } 51 Err(_) => { 52 let mut stmt = conn.prepare( 53 "SELECT id, COALESCE(title,''), COALESCE(file,''), COALESCE(level,0), COALESCE(pos,0) FROM nodes", 54 )?; 55 let iter = stmt.query_map([], |row| { 56 Ok(( 57 row.get::<_, String>(0)?, 58 row.get::<_, String>(1)?, 59 row.get::<_, String>(2)?, 60 row.get::<_, i64>(3)?, 61 row.get::<_, i64>(4)?, 62 )) 63 })?; 64 for row in iter.flatten() { 65 let (id, title, file, level, pos) = row; 66 let title = title.trim_matches('"').to_string(); 67 let file = file.trim_matches('"').to_string(); 68 nodes.insert(id.clone(), 69 RawNode { id, title, file, level, pos, mtime: 0, aliases: vec![], tags: vec![] }); 70 } 71 } 72 } 73 74 // Edges 75 let mut edges: Vec<(String, String)> = Vec::new(); 76 if let Ok(mut stmt) = conn.prepare("SELECT source, dest FROM links") { 77 let iter = stmt.query_map([], |row| { 78 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) 79 }); 80 if let Ok(iter) = iter { 81 for row in iter.flatten() { 82 if nodes.contains_key(&row.0) && nodes.contains_key(&row.1) { 83 edges.push(row); 84 } 85 } 86 } 87 } 88 89 // Aliases 90 if let Ok(mut stmt) = conn.prepare("SELECT node_id, alias FROM aliases") { 91 let iter = stmt.query_map([], |row| { 92 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) 93 }); 94 if let Ok(iter) = iter { 95 for row in iter.flatten() { 96 if let Some(n) = nodes.get_mut(&row.0) { 97 let alias = row.1.trim_matches('"').to_string(); 98 if !alias.is_empty() { 99 n.aliases.push(alias); 100 } 101 } 102 } 103 } 104 } 105 106 // Tags 107 if let Ok(mut stmt) = conn.prepare("SELECT node_id, tag FROM tags") { 108 let iter = stmt.query_map([], |row| { 109 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) 110 }); 111 if let Ok(iter) = iter { 112 for row in iter.flatten() { 113 if let Some(n) = nodes.get_mut(&row.0) { 114 if !row.1.is_empty() { 115 n.tags.push(row.1); 116 } 117 } 118 } 119 } 120 } 121 122 let node_list: Vec<RawNode> = nodes.into_values().collect(); 123 Ok((node_list, edges)) 124 } 125 126 /// Parse Emacs mtime which may be: 127 /// - A plain integer string "1779015285" 128 /// - Emacs internal time "(HIGH LOW MICROSEC PICOSEC)" → HIGH*65536 + LOW 129 /// - nil / empty → 0 130 fn parse_mtime(raw: Option<&str>) -> i64 { 131 let s = match raw { Some(s) if !s.is_empty() => s, _ => return 0 }; 132 // Try plain integer first 133 if let Ok(n) = s.trim().parse::<i64>() { return n; } 134 // Emacs list: strip parens, split on whitespace, take HIGH and LOW 135 let inner = s.trim().trim_start_matches('(').trim_end_matches(')'); 136 let parts: Vec<&str> = inner.split_whitespace().collect(); 137 if parts.len() >= 2 { 138 let high: i64 = parts[0].parse().unwrap_or(0); 139 let low: i64 = parts[1].parse().unwrap_or(0); 140 return high * 65536 + low; 141 } 142 0 143 }