infigraph_core/graph/
store.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use anyhow::Result;
5use fs2::FileExt;
6use kuzu::{Connection, Database, SystemConfig};
7
8use super::schema::{CREATE_SCHEMA, MIGRATIONS};
9use super::store_util::escape;
10
11pub struct WriteLock {
14 _file: std::fs::File,
15}
16
17impl WriteLock {
18 fn acquire(lock_path: &Path) -> Result<Self> {
19 if let Some(parent) = lock_path.parent() {
20 std::fs::create_dir_all(parent)?;
21 }
22 let file = std::fs::OpenOptions::new()
23 .create(true)
24 .write(true)
25 .truncate(false)
26 .open(lock_path)?;
27 file.lock_exclusive()
28 .map_err(|e| anyhow::anyhow!("failed to acquire write lock: {e}"))?;
29 Ok(Self { _file: file })
30 }
31
32 fn try_acquire(lock_path: &Path) -> Result<Option<Self>> {
33 if let Some(parent) = lock_path.parent() {
34 std::fs::create_dir_all(parent)?;
35 }
36 let file = std::fs::OpenOptions::new()
37 .create(true)
38 .write(true)
39 .truncate(false)
40 .open(lock_path)?;
41 match file.try_lock_exclusive() {
42 Ok(()) => Ok(Some(Self { _file: file })),
43 Err(ref e)
44 if e.kind() == std::io::ErrorKind::WouldBlock || e.raw_os_error() == Some(33) =>
45 {
46 Ok(None)
47 }
48 Err(e) => Err(anyhow::anyhow!("lock error: {e}")),
49 }
50 }
51}
52
53pub struct GraphStore {
55 db: Database,
56 lock_path: PathBuf,
57}
58
59impl GraphStore {
60 pub fn open(path: &Path) -> Result<Self> {
62 if let Some(parent) = path.parent() {
63 std::fs::create_dir_all(parent)?;
64 }
65 let lock_path = path.with_extension("lock");
66 let db = Database::new(path, SystemConfig::default())
67 .map_err(|e| anyhow::anyhow!("failed to open kuzu db: {e}"))?;
68 let store = Self { db, lock_path };
69 store.init_schema()?;
70 Ok(store)
71 }
72
73 pub fn write_lock(&self) -> Result<WriteLock> {
75 WriteLock::acquire(&self.lock_path)
76 }
77
78 pub fn try_write_lock(&self) -> Result<Option<WriteLock>> {
80 WriteLock::try_acquire(&self.lock_path)
81 }
82
83 fn init_schema(&self) -> Result<()> {
84 let conn = self.connection()?;
85 for ddl in CREATE_SCHEMA {
86 conn.query(ddl)
87 .map_err(|e| anyhow::anyhow!("schema error: {e}\n DDL: {ddl}"))?;
88 }
89 for migration in MIGRATIONS {
90 let _ = conn.query(migration);
91 }
92 Ok(())
93 }
94
95 pub fn connection(&self) -> Result<Connection<'_>> {
96 Connection::new(&self.db).map_err(|e| anyhow::anyhow!("failed to create connection: {e}"))
97 }
98
99 pub fn remove_file(&self, file: &str) -> Result<()> {
101 let _lock = self.write_lock()?;
102 let conn = self.connection()?;
103 self.remove_file_conn(&conn, file)
104 }
105
106 pub fn remove_file_conn(&self, conn: &Connection<'_>, file: &str) -> Result<()> {
108 let _ = conn.query(&format!(
109 "MATCH (f:File)-[:DEFINES]->(s:Symbol)-[:HAS_STATEMENT]->(st:Statement) WHERE f.id = '{}' DETACH DELETE st",
110 escape(file)
111 ));
112 let _ = conn.query(&format!(
113 "MATCH (s:Symbol) WHERE s.file = '{}' DETACH DELETE s",
114 escape(file)
115 ));
116 let _ = conn.query(&format!(
117 "MATCH (m:Module) WHERE m.file = '{}' DETACH DELETE m",
118 escape(file)
119 ));
120 let _ = conn.query(&format!(
121 "MATCH (f:File) WHERE f.id = '{}' DETACH DELETE f",
122 escape(file)
123 ));
124 Ok(())
125 }
126
127 pub fn get_file_hashes(&self) -> Result<HashMap<String, String>> {
130 let conn = self.connection()?;
131 let result = conn
132 .query("MATCH (m:Module) RETURN m.file, m.content_hash")
133 .map_err(|e| anyhow::anyhow!("get_file_hashes failed: {e}"))?;
134 let mut map = HashMap::new();
135 for row in result {
136 if row.len() >= 2 {
137 map.insert(row[0].to_string(), row[1].to_string());
138 }
139 }
140 Ok(map)
141 }
142
143 pub fn get_all_symbols(&self) -> Result<Vec<(String, String, String, String)>> {
145 let conn = self.connection()?;
146 let result = conn
147 .query("MATCH (s:Symbol) RETURN s.name, s.id, s.file, s.kind")
148 .map_err(|e| anyhow::anyhow!("get_all_symbols failed: {e}"))?;
149 let mut symbols = Vec::new();
150 for row in result {
151 if row.len() >= 4 {
152 symbols.push((
153 row[0].to_string(),
154 row[1].to_string(),
155 row[2].to_string(),
156 row[3].to_string(),
157 ));
158 }
159 }
160 Ok(symbols)
161 }
162
163 pub fn derive_tested_by_edges(&self) -> Result<usize> {
165 let _lock = self.write_lock()?;
166 let conn = self.connection()?;
167 let q = super::queries::GraphQuery::new(&conn);
168 q.derive_tested_by_edges()
169 }
170
171 pub fn stats(&self) -> Result<GraphStats> {
172 let conn = self.connection()?;
173
174 let symbol_count = count_query(&conn, "MATCH (s:Symbol) RETURN count(s)")?;
175 let module_count = count_query(&conn, "MATCH (m:Module) RETURN count(m)")?;
176 let file_count = count_query(&conn, "MATCH (f:File) RETURN count(f)")?;
177 let folder_count = count_query(&conn, "MATCH (d:Folder) RETURN count(d)")?;
178 let calls_count = count_query(&conn, "MATCH ()-[r:CALLS]->() RETURN count(r)")?;
179 let inherits_count = count_query(&conn, "MATCH ()-[r:INHERITS]->() RETURN count(r)")?;
180 let contains_count = count_query(&conn, "MATCH ()-[r:CONTAINS]->() RETURN count(r)")?;
181
182 Ok(GraphStats {
183 symbols: symbol_count,
184 modules: module_count,
185 files: file_count,
186 folders: folder_count,
187 calls: calls_count,
188 inherits: inherits_count,
189 contains: contains_count,
190 })
191 }
192}
193
194#[derive(Debug)]
195pub struct GraphStats {
196 pub symbols: u64,
197 pub modules: u64,
198 pub files: u64,
199 pub folders: u64,
200 pub calls: u64,
201 pub inherits: u64,
202 pub contains: u64,
203}
204
205impl std::fmt::Display for GraphStats {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 writeln!(f, "Graph Statistics:")?;
208 writeln!(f, " Symbols: {}", self.symbols)?;
209 writeln!(f, " Modules: {}", self.modules)?;
210 writeln!(f, " Files: {}", self.files)?;
211 writeln!(f, " Folders: {}", self.folders)?;
212 writeln!(f, " Calls edges: {}", self.calls)?;
213 writeln!(f, " Inherits: {}", self.inherits)?;
214 writeln!(f, " Contains: {}", self.contains)
215 }
216}
217
218fn count_query(conn: &Connection, query: &str) -> Result<u64> {
219 let mut result = conn
220 .query(query)
221 .map_err(|e| anyhow::anyhow!("query failed: {e}"))?;
222 if let Some(row) = result.next() {
223 if let Some(val) = row.first() {
224 return Ok(val.to_string().parse().unwrap_or(0));
225 }
226 }
227 Ok(0)
228}