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