1use std::path::Path;
4
5use rusqlite::{Connection, OptionalExtension, params};
6
7use crate::migrations;
8use crate::model::{Direction, Edge, EdgeKind, FactSet, Node, NodeKind, Span};
9use crate::provenance::Provenance;
10
11#[derive(Debug, thiserror::Error)]
13pub enum StoreError {
14 #[error("sqlite error: {0}")]
16 Sqlite(#[from] rusqlite::Error),
17 #[error("json error: {0}")]
19 Json(#[from] serde_json::Error),
20 #[error("unknown node key: {0}")]
22 UnknownNode(String),
23 #[error("invalid edge: {0}")]
25 InvalidEdge(String),
26 #[error("corrupt store: {0}")]
28 Corrupt(String),
29}
30
31const NODE_COLS: &str =
33 "n.key, n.kind, n.name, n.path, n.lang, n.blob_hash, n.span_start, n.span_end, n.meta";
34
35const EDGE_SELECT: &str = "SELECT ns.key AS src, nd.key AS dst, e.kind, e.provenance, \
37 e.confidence, e.src_ref \
38 FROM edges e JOIN nodes ns ON ns.id = e.src JOIN nodes nd ON nd.id = e.dst";
39
40pub struct Store {
42 conn: Connection,
43}
44
45impl Store {
46 pub fn open(path: &Path) -> Result<Self, StoreError> {
52 let conn = Connection::open(path)?;
53 Self::from_conn(conn)
54 }
55
56 pub fn open_in_memory() -> Result<Self, StoreError> {
61 let conn = Connection::open_in_memory()?;
62 Self::from_conn(conn)
63 }
64
65 fn from_conn(mut conn: Connection) -> Result<Self, StoreError> {
66 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
67 migrations::apply(&mut conn)?;
68 Ok(Self { conn })
69 }
70
71 pub fn schema_version(&self) -> Result<u32, StoreError> {
76 let v: i64 = self.conn.query_row(
77 "SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
78 [],
79 |r| r.get(0),
80 )?;
81 Ok(u32::try_from(v).unwrap_or(0))
82 }
83
84 pub fn node_count(&self) -> Result<u64, StoreError> {
89 let n: i64 = self
90 .conn
91 .query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))?;
92 Ok(u64::try_from(n).unwrap_or(0))
93 }
94
95 pub fn edge_count(&self) -> Result<u64, StoreError> {
100 let n: i64 = self
101 .conn
102 .query_row("SELECT COUNT(*) FROM edges", [], |r| r.get(0))?;
103 Ok(u64::try_from(n).unwrap_or(0))
104 }
105
106 pub fn upsert_node(&self, node: &Node) -> Result<(), StoreError> {
112 upsert_node(&self.conn, node)
113 }
114
115 pub fn insert_edge(&self, edge: &Edge) -> Result<(), StoreError> {
122 insert_edge(&self.conn, edge)
123 }
124
125 pub fn apply_factset(&mut self, facts: &FactSet) -> Result<(), StoreError> {
132 let tx = self.conn.transaction()?;
133 for node in &facts.nodes {
134 upsert_node(&tx, node)?;
135 }
136 for edge in &facts.edges {
137 insert_edge(&tx, edge)?;
138 }
139 tx.commit()?;
140 Ok(())
141 }
142
143 pub fn sync_state(&self) -> Result<Option<String>, StoreError> {
149 Ok(self
150 .conn
151 .query_row("SELECT tree FROM sync_state WHERE id = 0", [], |r| r.get(0))
152 .optional()?)
153 }
154
155 pub fn rebuild(&mut self, facts: &FactSet, tree: &str) -> Result<(), StoreError> {
163 let tx = self.conn.transaction()?;
164 tx.execute("DELETE FROM edges", [])?;
165 tx.execute("DELETE FROM nodes", [])?;
166 for node in &facts.nodes {
167 upsert_node(&tx, node)?;
168 }
169 for edge in &facts.edges {
170 insert_edge(&tx, edge)?;
171 }
172 tx.execute(
173 "INSERT INTO sync_state (id, tree) VALUES (0, ?1)
174 ON CONFLICT(id) DO UPDATE SET tree = excluded.tree",
175 [tree],
176 )?;
177 tx.commit()?;
178 Ok(())
179 }
180
181 pub fn get_node(&self, key: &str) -> Result<Option<Node>, StoreError> {
187 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.key = ?1");
188 let mut stmt = self.conn.prepare(&sql)?;
189 let mut rows = stmt.query([key])?;
190 match rows.next()? {
191 Some(row) => Ok(Some(row_to_node(row)?)),
192 None => Ok(None),
193 }
194 }
195
196 pub fn all_keys(&self) -> Result<Vec<String>, StoreError> {
201 let mut stmt = self.conn.prepare("SELECT key FROM nodes ORDER BY key")?;
202 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
203 let mut out = Vec::new();
204 for row in rows {
205 out.push(row?);
206 }
207 Ok(out)
208 }
209
210 pub fn nodes_by_kind(&self, kind: &NodeKind) -> Result<Vec<Node>, StoreError> {
216 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.kind = ?1 ORDER BY n.key");
217 let mut stmt = self.conn.prepare(&sql)?;
218 let mut rows = stmt.query([kind.as_str()])?;
219 collect_nodes(&mut rows)
220 }
221
222 pub fn edges_from(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
227 let sql = format!("{EDGE_SELECT} WHERE ns.key = ?1 ORDER BY e.id");
228 let mut stmt = self.conn.prepare(&sql)?;
229 let mut rows = stmt.query([key])?;
230 collect_edges(&mut rows)
231 }
232
233 pub fn edges_to(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
238 let sql = format!("{EDGE_SELECT} WHERE nd.key = ?1 ORDER BY e.id");
239 let mut stmt = self.conn.prepare(&sql)?;
240 let mut rows = stmt.query([key])?;
241 collect_edges(&mut rows)
242 }
243
244 pub fn edges_by_provenance(&self, provenance: Provenance) -> Result<Vec<Edge>, StoreError> {
249 let sql = format!("{EDGE_SELECT} WHERE e.provenance = ?1 ORDER BY e.id");
250 let mut stmt = self.conn.prepare(&sql)?;
251 let mut rows = stmt.query([provenance.as_str()])?;
252 collect_edges(&mut rows)
253 }
254
255 pub fn neighbors(&self, key: &str, dir: Direction) -> Result<Vec<Node>, StoreError> {
262 let out = format!(
263 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.dst \
264 JOIN nodes s ON s.id = e.src WHERE s.key = ?1"
265 );
266 let inc = format!(
267 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.src \
268 JOIN nodes d ON d.id = e.dst WHERE d.key = ?1"
269 );
270 let sql = match dir {
275 Direction::Outgoing => format!("{out} ORDER BY 1"),
276 Direction::Incoming => format!("{inc} ORDER BY 1"),
277 Direction::Both => format!("{out} UNION {inc} ORDER BY 1"),
278 };
279 let mut stmt = self.conn.prepare(&sql)?;
280 let mut rows = stmt.query([key])?;
281 collect_nodes(&mut rows)
282 }
283}
284
285fn node_row_id(conn: &Connection, key: &str) -> rusqlite::Result<Option<i64>> {
288 conn.query_row("SELECT id FROM nodes WHERE key = ?1", [key], |r| r.get(0))
289 .optional()
290}
291
292fn upsert_node(conn: &Connection, node: &Node) -> Result<(), StoreError> {
293 let meta = serde_json::to_string(&node.meta)?;
294 let (span_start, span_end) = match node.span {
295 Some(s) => (Some(i64::from(s.start)), Some(i64::from(s.end))),
296 None => (None, None),
297 };
298 conn.execute(
299 "INSERT INTO nodes (key, kind, name, path, lang, blob_hash, span_start, span_end, meta)
300 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
301 ON CONFLICT(key) DO UPDATE SET
302 kind = excluded.kind, name = excluded.name, path = excluded.path,
303 lang = excluded.lang, blob_hash = excluded.blob_hash,
304 span_start = excluded.span_start, span_end = excluded.span_end,
305 meta = excluded.meta",
306 params![
307 node.key,
308 node.kind.as_str(),
309 node.name,
310 node.path,
311 node.lang,
312 node.blob_hash,
313 span_start,
314 span_end,
315 meta,
316 ],
317 )?;
318 Ok(())
319}
320
321fn insert_edge(conn: &Connection, edge: &Edge) -> Result<(), StoreError> {
322 if !edge.is_valid() {
323 return Err(StoreError::InvalidEdge(format!(
324 "confidence must be present iff provenance is inferred (src={}, dst={})",
325 edge.src, edge.dst
326 )));
327 }
328 let src_id =
329 node_row_id(conn, &edge.src)?.ok_or_else(|| StoreError::UnknownNode(edge.src.clone()))?;
330 let dst_id =
331 node_row_id(conn, &edge.dst)?.ok_or_else(|| StoreError::UnknownNode(edge.dst.clone()))?;
332 conn.execute(
337 "INSERT INTO edges (src, dst, kind, provenance, confidence, src_ref)
338 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
339 ON CONFLICT(src, dst, kind, provenance) DO NOTHING",
340 params![
341 src_id,
342 dst_id,
343 edge.kind.as_str(),
344 edge.provenance.as_str(),
345 edge.confidence,
346 edge.src_ref,
347 ],
348 )?;
349 Ok(())
350}
351
352fn collect_nodes(rows: &mut rusqlite::Rows) -> Result<Vec<Node>, StoreError> {
353 let mut out = Vec::new();
354 while let Some(row) = rows.next()? {
355 out.push(row_to_node(row)?);
356 }
357 Ok(out)
358}
359
360fn collect_edges(rows: &mut rusqlite::Rows) -> Result<Vec<Edge>, StoreError> {
361 let mut out = Vec::new();
362 while let Some(row) = rows.next()? {
363 out.push(row_to_edge(row)?);
364 }
365 Ok(out)
366}
367
368fn row_to_node(row: &rusqlite::Row) -> Result<Node, StoreError> {
369 let kind: String = row.get("kind")?;
370 let span_start: Option<i64> = row.get("span_start")?;
371 let span_end: Option<i64> = row.get("span_end")?;
372 let span = match (span_start, span_end) {
373 (Some(s), Some(e)) => Some(Span::new(to_u32(s)?, to_u32(e)?)),
374 _ => None,
375 };
376 let meta: String = row.get("meta")?;
377 Ok(Node {
378 key: row.get("key")?,
379 kind: NodeKind::from_token(&kind),
380 name: row.get("name")?,
381 path: row.get("path")?,
382 lang: row.get("lang")?,
383 blob_hash: row.get("blob_hash")?,
384 span,
385 meta: serde_json::from_str(&meta)?,
386 })
387}
388
389fn row_to_edge(row: &rusqlite::Row) -> Result<Edge, StoreError> {
390 let kind: String = row.get("kind")?;
391 let provenance: String = row.get("provenance")?;
392 let provenance = Provenance::from_token(&provenance)
393 .ok_or_else(|| StoreError::Corrupt(format!("unknown provenance: {provenance}")))?;
394 Ok(Edge {
395 src: row.get("src")?,
396 dst: row.get("dst")?,
397 kind: EdgeKind::from_token(&kind),
398 provenance,
399 confidence: row.get("confidence")?,
400 src_ref: row.get("src_ref")?,
401 })
402}
403
404fn to_u32(v: i64) -> Result<u32, StoreError> {
405 u32::try_from(v).map_err(|_| StoreError::Corrupt(format!("span offset out of range: {v}")))
406}
407
408#[cfg(test)]
409mod tests {
410 use super::Store;
411 use crate::model::{Direction, Edge, EdgeKind, FactSet, Node, NodeKind, Span};
412 use crate::provenance::Provenance;
413
414 fn sample_node(key: &str) -> Node {
415 Node {
416 key: key.to_owned(),
417 kind: NodeKind::Fn,
418 name: "sample".to_owned(),
419 path: Some("src/lib.rs".to_owned()),
420 lang: Some("rust".to_owned()),
421 blob_hash: Some("deadbeef".to_owned()),
422 span: Some(Span::new(10, 42)),
423 meta: serde_json::json!({"vis": "pub"}),
424 }
425 }
426
427 #[test]
428 fn open_in_memory_applies_schema() {
429 let store = Store::open_in_memory().expect("open");
430 assert_eq!(store.node_count().expect("count"), 0);
431 assert_eq!(store.schema_version().expect("version"), 3);
432 }
433
434 #[test]
435 fn upsert_and_get_round_trips_all_fields() {
436 let store = Store::open_in_memory().expect("open");
437 let node = sample_node("sym:rust:src/lib.rs#sample");
438 store.upsert_node(&node).expect("upsert");
439 let got = store.get_node(&node.key).expect("get").expect("present");
440 assert_eq!(got, node);
441 }
442
443 #[test]
444 fn upsert_updates_in_place() {
445 let store = Store::open_in_memory().expect("open");
446 let mut node = sample_node("k");
447 store.upsert_node(&node).expect("insert");
448 node.name = "renamed".to_owned();
449 node.kind = NodeKind::Struct;
450 store.upsert_node(&node).expect("update");
451 assert_eq!(store.node_count().expect("count"), 1);
452 let got = store.get_node("k").expect("get").expect("present");
453 assert_eq!(got.name, "renamed");
454 assert_eq!(got.kind, NodeKind::Struct);
455 }
456
457 #[test]
458 fn edge_with_unknown_endpoint_is_rejected() {
459 let store = Store::open_in_memory().expect("open");
460 store
461 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
462 .expect("a");
463 let edge = Edge::derived("a", "missing", EdgeKind::Calls);
464 let err = store.insert_edge(&edge).expect_err("should reject");
465 assert!(matches!(err, super::StoreError::UnknownNode(k) if k == "missing"));
466 }
467
468 #[test]
469 fn inferred_edge_requires_confidence() {
470 let store = Store::open_in_memory().expect("open");
471 store
472 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
473 .expect("a");
474 store
475 .upsert_node(&Node::new("b", NodeKind::Fn, "b"))
476 .expect("b");
477 let bad = Edge {
479 src: "a".to_owned(),
480 dst: "b".to_owned(),
481 kind: EdgeKind::References,
482 provenance: Provenance::Inferred,
483 confidence: None,
484 src_ref: None,
485 };
486 assert!(matches!(
487 store.insert_edge(&bad).expect_err("reject"),
488 super::StoreError::InvalidEdge(_)
489 ));
490 }
491
492 #[test]
493 fn apply_factset_is_atomic() {
494 let mut store = Store::open_in_memory().expect("open");
495 let facts = FactSet::new()
497 .with_node(Node::new("a", NodeKind::Fn, "a"))
498 .with_node(Node::new("b", NodeKind::Fn, "b"))
499 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
500 .with_edge(Edge::derived("a", "ghost", EdgeKind::Calls));
501 assert!(store.apply_factset(&facts).is_err());
502 assert_eq!(store.node_count().expect("count"), 0, "rolled back");
503 assert_eq!(store.edge_count().expect("count"), 0, "rolled back");
504 }
505
506 #[test]
507 fn neighbors_and_provenance_queries() {
508 let mut store = Store::open_in_memory().expect("open");
509 let facts = FactSet::new()
510 .with_node(Node::new("a", NodeKind::Fn, "a"))
511 .with_node(Node::new("b", NodeKind::Fn, "b"))
512 .with_node(Node::new("c", NodeKind::Fn, "c"))
513 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
514 .with_edge(Edge::inferred("a", "c", EdgeKind::References, 0.5));
515 store.apply_factset(&facts).expect("apply");
516
517 let out = store.neighbors("a", Direction::Outgoing).expect("out");
518 let mut keys: Vec<_> = out.iter().map(|n| n.key.clone()).collect();
519 keys.sort();
520 assert_eq!(keys, ["b", "c"]);
521
522 assert!(
523 store
524 .neighbors("b", Direction::Outgoing)
525 .expect("b out")
526 .is_empty()
527 );
528 assert_eq!(
529 store
530 .neighbors("b", Direction::Incoming)
531 .expect("b in")
532 .len(),
533 1
534 );
535
536 let inferred = store
537 .edges_by_provenance(Provenance::Inferred)
538 .expect("inf");
539 assert_eq!(inferred.len(), 1);
540 assert_eq!(inferred[0].confidence, Some(0.5));
541 }
542
543 #[test]
544 fn neighbors_of_absent_node_is_empty() {
545 let store = Store::open_in_memory().expect("open");
546 assert!(
547 store
548 .neighbors("nope", Direction::Both)
549 .expect("q")
550 .is_empty()
551 );
552 }
553
554 #[test]
555 fn get_missing_node_is_none() {
556 let store = Store::open_in_memory().expect("open");
557 assert!(store.get_node("absent").expect("get").is_none());
558 }
559
560 #[test]
561 fn nodes_by_kind_and_edges_to() {
562 let mut store = Store::open_in_memory().expect("open");
563 let facts = FactSet::new()
564 .with_node(Node::new("f1", NodeKind::Fn, "f1"))
565 .with_node(Node::new("f2", NodeKind::Fn, "f2"))
566 .with_node(Node::new("s1", NodeKind::Struct, "s1"))
567 .with_edge(Edge::derived("f1", "s1", EdgeKind::References))
568 .with_edge(Edge::derived("f2", "s1", EdgeKind::References));
569 store.apply_factset(&facts).expect("apply");
570
571 let fns = store.nodes_by_kind(&NodeKind::Fn).expect("fns");
572 assert_eq!(
573 fns.iter().map(|n| n.key.as_str()).collect::<Vec<_>>(),
574 ["f1", "f2"]
575 );
576 assert!(
577 store
578 .nodes_by_kind(&NodeKind::Enum)
579 .expect("enums")
580 .is_empty()
581 );
582
583 let into_s1 = store.edges_to("s1").expect("edges_to");
584 assert_eq!(into_s1.len(), 2);
585 assert!(into_s1.iter().all(|e| e.dst == "s1"));
586 }
587
588 #[test]
589 fn open_persists_across_reopen() {
590 let path =
591 std::env::temp_dir().join(format!("roteiro-open-test-{}.db", std::process::id()));
592 std::fs::remove_file(&path).ok();
593 {
594 let store = Store::open(&path).expect("open");
595 store
596 .upsert_node(&sample_node("persisted"))
597 .expect("upsert");
598 }
599 {
600 let store = Store::open(&path).expect("reopen");
601 assert_eq!(store.node_count().expect("count"), 1);
602 assert_eq!(store.schema_version().expect("version"), 3);
603 assert!(store.get_node("persisted").expect("get").is_some());
604 }
605 std::fs::remove_file(&path).expect("cleanup");
606 }
607}