1use gonzalo_graph::{CodeGraph, GraphStore, Located, RefKind, Reference, Symbol};
17use rusqlite::{Connection, params};
18use std::path::Path;
19use std::sync::Mutex;
20
21const SCHEMA: &str = "
22CREATE TABLE IF NOT EXISTS symbols (
23 path TEXT NOT NULL,
24 name TEXT NOT NULL,
25 kind TEXT NOT NULL,
26 start_line INTEGER NOT NULL,
27 end_line INTEGER NOT NULL
28);
29CREATE TABLE IF NOT EXISTS refs (
30 path TEXT NOT NULL,
31 name TEXT NOT NULL,
32 from_fn TEXT,
33 line INTEGER NOT NULL,
34 kind TEXT NOT NULL DEFAULT 'free'
35);
36CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
37CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
38CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name);
39CREATE INDEX IF NOT EXISTS idx_refs_from ON refs(from_fn);
40CREATE INDEX IF NOT EXISTS idx_refs_path ON refs(path);
41";
42
43pub struct SqliteGraphStore {
45 conn: Mutex<Connection>,
46}
47
48impl SqliteGraphStore {
49 pub fn open(path: impl AsRef<Path>) -> rusqlite::Result<Self> {
52 let path = path.as_ref();
53 if let Some(parent) = path.parent() {
54 let _ = std::fs::create_dir_all(parent);
56 }
57 Self::init(Connection::open(path)?)
58 }
59
60 pub fn open_in_memory() -> rusqlite::Result<Self> {
62 Self::init(Connection::open_in_memory()?)
63 }
64
65 fn init(conn: Connection) -> rusqlite::Result<Self> {
66 conn.execute_batch(SCHEMA)?;
67 Self::migrate(&conn)?;
68 Ok(Self {
69 conn: Mutex::new(conn),
70 })
71 }
72
73 fn migrate(conn: &Connection) -> rusqlite::Result<()> {
80 let has_kind = conn
81 .prepare("SELECT 1 FROM pragma_table_info('refs') WHERE name = 'kind'")?
82 .exists([])?;
83 if !has_kind {
84 conn.execute_batch("ALTER TABLE refs ADD COLUMN kind TEXT NOT NULL DEFAULT 'free'")?;
85 }
86 Ok(())
87 }
88
89 pub fn remove_path(&mut self, path: &str) {
92 let guard = self.conn.lock().expect("connection poisoned");
93 guard
94 .execute("DELETE FROM symbols WHERE path = ?1", params![path])
95 .expect("delete symbols for path");
96 guard
97 .execute("DELETE FROM refs WHERE path = ?1", params![path])
98 .expect("delete refs for path");
99 }
100}
101
102pub fn view_db_path(graph_root: &Path, repo: &str, view_id: &str) -> std::path::PathBuf {
106 graph_root
107 .join(fs_safe(repo))
108 .join(format!("{}.db", fs_safe(view_id)))
109}
110
111fn fs_safe(s: &str) -> String {
123 gonzalo_core::segment(s)
124}
125
126fn kind_to_text(sym: &Symbol) -> String {
128 serde_json::to_string(&sym.kind).expect("SymbolKind serializes")
129}
130
131fn symbol_from_row(row: &rusqlite::Row, base: usize) -> rusqlite::Result<Symbol> {
132 let name: String = row.get(base)?;
133 let kind_text: String = row.get(base + 1)?;
134 let start: i64 = row.get(base + 2)?;
135 let end: i64 = row.get(base + 3)?;
136 Ok(Symbol {
137 name,
138 kind: serde_json::from_str(&kind_text).expect("stored SymbolKind is valid"),
139 start_line: start as usize,
140 end_line: end as usize,
141 })
142}
143
144impl GraphStore for SqliteGraphStore {
145 fn insert(&mut self, path: &str, graph: CodeGraph) {
146 let mut guard = self.conn.lock().expect("connection poisoned");
147 let tx = guard.transaction().expect("begin transaction");
148 tx.execute("DELETE FROM symbols WHERE path = ?1", params![path])
149 .expect("clear symbols for path");
150 tx.execute("DELETE FROM refs WHERE path = ?1", params![path])
151 .expect("clear refs for path");
152 for s in &graph.symbols {
153 tx.execute(
154 "INSERT INTO symbols (path, name, kind, start_line, end_line)
155 VALUES (?1, ?2, ?3, ?4, ?5)",
156 params![
157 path,
158 s.name,
159 kind_to_text(s),
160 s.start_line as i64,
161 s.end_line as i64
162 ],
163 )
164 .expect("insert symbol");
165 }
166 for r in &graph.references {
167 tx.execute(
168 "INSERT INTO refs (path, name, from_fn, line, kind) VALUES (?1, ?2, ?3, ?4, ?5)",
169 params![path, r.name, r.from, r.line as i64, r.kind.as_str()],
170 )
171 .expect("insert reference");
172 }
173 tx.commit().expect("commit transaction");
174 }
175
176 fn symbols_in_file(&self, path: &str) -> Vec<Symbol> {
177 let guard = self.conn.lock().expect("connection poisoned");
178 let mut stmt = guard
179 .prepare("SELECT name, kind, start_line, end_line FROM symbols WHERE path = ?1")
180 .expect("prepare symbols_in_file");
181 let rows = stmt
182 .query_map(params![path], |row| symbol_from_row(row, 0))
183 .expect("query symbols_in_file");
184 rows.collect::<rusqlite::Result<Vec<_>>>()
185 .expect("collect symbols_in_file")
186 }
187
188 fn definitions(&self, name: &str) -> Vec<Located<Symbol>> {
189 let guard = self.conn.lock().expect("connection poisoned");
190 let mut stmt = guard
191 .prepare(
192 "SELECT path, name, kind, start_line, end_line FROM symbols
193 WHERE name = ?1 ORDER BY path",
194 )
195 .expect("prepare definitions");
196 let rows = stmt
197 .query_map(params![name], |row| {
198 Ok(Located {
199 path: row.get(0)?,
200 item: symbol_from_row(row, 1)?,
201 })
202 })
203 .expect("query definitions");
204 rows.collect::<rusqlite::Result<Vec<_>>>()
205 .expect("collect definitions")
206 }
207
208 fn references_to(&self, name: &str) -> Vec<Located<Reference>> {
209 let guard = self.conn.lock().expect("connection poisoned");
210 let mut stmt = guard
211 .prepare(
212 "SELECT path, name, from_fn, line, kind FROM refs
213 WHERE name = ?1 ORDER BY path, line",
214 )
215 .expect("prepare references_to");
216 let rows = stmt
217 .query_map(params![name], |row| {
218 Ok(Located {
219 path: row.get(0)?,
220 item: Reference {
221 name: row.get(1)?,
222 from: row.get::<_, Option<String>>(2)?,
223 line: row.get::<_, i64>(3)? as usize,
224 kind: RefKind::from_str_or_free(&row.get::<_, String>(4)?),
225 },
226 })
227 })
228 .expect("query references_to");
229 rows.collect::<rusqlite::Result<Vec<_>>>()
230 .expect("collect references_to")
231 }
232
233 fn callers_of(&self, name: &str) -> Vec<String> {
234 let guard = self.conn.lock().expect("connection poisoned");
235 let mut stmt = guard
236 .prepare(
237 "SELECT DISTINCT from_fn FROM refs
238 WHERE name = ?1 AND from_fn IS NOT NULL ORDER BY from_fn",
239 )
240 .expect("prepare callers_of");
241 let rows = stmt
242 .query_map(params![name], |row| row.get::<_, String>(0))
243 .expect("query callers_of");
244 rows.collect::<rusqlite::Result<Vec<_>>>()
245 .expect("collect callers_of")
246 }
247
248 fn callees(&self, name: &str) -> Vec<String> {
249 let guard = self.conn.lock().expect("connection poisoned");
250 let mut stmt = guard
251 .prepare("SELECT DISTINCT name FROM refs WHERE from_fn = ?1 ORDER BY name")
252 .expect("prepare callees");
253 let rows = stmt
254 .query_map(params![name], |row| row.get::<_, String>(0))
255 .expect("query callees");
256 rows.collect::<rusqlite::Result<Vec<_>>>()
257 .expect("collect callees")
258 }
259
260 fn all_symbols(&self) -> Vec<Located<Symbol>> {
261 let guard = self.conn.lock().expect("connection poisoned");
262 let mut stmt = guard
263 .prepare("SELECT path, name, kind, start_line, end_line FROM symbols ORDER BY path")
264 .expect("prepare all_symbols");
265 let rows = stmt
266 .query_map([], |row| {
267 Ok(Located {
268 path: row.get(0)?,
269 item: symbol_from_row(row, 1)?,
270 })
271 })
272 .expect("query all_symbols");
273 rows.collect::<rusqlite::Result<Vec<_>>>()
274 .expect("collect all_symbols")
275 }
276
277 fn all_references(&self) -> Vec<Located<Reference>> {
278 let guard = self.conn.lock().expect("connection poisoned");
279 let mut stmt = guard
280 .prepare("SELECT path, name, from_fn, line, kind FROM refs ORDER BY path, line")
281 .expect("prepare all_references");
282 let rows = stmt
283 .query_map([], |row| {
284 Ok(Located {
285 path: row.get(0)?,
286 item: Reference {
287 name: row.get(1)?,
288 from: row.get::<_, Option<String>>(2)?,
289 line: row.get::<_, i64>(3)? as usize,
290 kind: RefKind::from_str_or_free(&row.get::<_, String>(4)?),
291 },
292 })
293 })
294 .expect("query all_references");
295 rows.collect::<rusqlite::Result<Vec<_>>>()
296 .expect("collect all_references")
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
310 fn view_db_path_is_injective_across_colliding_pairs() {
311 let root = Path::new("/graphs");
312 let a = view_db_path(root, "org/repo", "main");
313 let b = view_db_path(root, "org_repo", "main");
314 assert_ne!(a, b, "distinct repos must not collide onto one db file");
315
316 assert_eq!(a, Path::new("/graphs/org%2Frepo/main.db"));
319 assert_eq!(b, Path::new("/graphs/org_repo/main.db"));
320 assert!(a.extension().is_some_and(|e| e == "db"));
321
322 let c = view_db_path(root, "org/repo", "v1.0");
324 let d = view_db_path(root, "org/repo", "v1_0");
325 assert_ne!(c, d, "distinct views must not collide onto one db file");
326 }
327}