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 nodes_by_kind(&self, kind: &NodeKind) -> Result<Vec<Node>, StoreError> {
202 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.kind = ?1 ORDER BY n.key");
203 let mut stmt = self.conn.prepare(&sql)?;
204 let mut rows = stmt.query([kind.as_str()])?;
205 collect_nodes(&mut rows)
206 }
207
208 pub fn edges_from(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
213 let sql = format!("{EDGE_SELECT} WHERE ns.key = ?1 ORDER BY e.id");
214 let mut stmt = self.conn.prepare(&sql)?;
215 let mut rows = stmt.query([key])?;
216 collect_edges(&mut rows)
217 }
218
219 pub fn edges_to(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
224 let sql = format!("{EDGE_SELECT} WHERE nd.key = ?1 ORDER BY e.id");
225 let mut stmt = self.conn.prepare(&sql)?;
226 let mut rows = stmt.query([key])?;
227 collect_edges(&mut rows)
228 }
229
230 pub fn edges_by_provenance(&self, provenance: Provenance) -> Result<Vec<Edge>, StoreError> {
235 let sql = format!("{EDGE_SELECT} WHERE e.provenance = ?1 ORDER BY e.id");
236 let mut stmt = self.conn.prepare(&sql)?;
237 let mut rows = stmt.query([provenance.as_str()])?;
238 collect_edges(&mut rows)
239 }
240
241 pub fn neighbors(&self, key: &str, dir: Direction) -> Result<Vec<Node>, StoreError> {
248 let out = format!(
249 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.dst \
250 JOIN nodes s ON s.id = e.src WHERE s.key = ?1"
251 );
252 let inc = format!(
253 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.src \
254 JOIN nodes d ON d.id = e.dst WHERE d.key = ?1"
255 );
256 let sql = match dir {
261 Direction::Outgoing => format!("{out} ORDER BY 1"),
262 Direction::Incoming => format!("{inc} ORDER BY 1"),
263 Direction::Both => format!("{out} UNION {inc} ORDER BY 1"),
264 };
265 let mut stmt = self.conn.prepare(&sql)?;
266 let mut rows = stmt.query([key])?;
267 collect_nodes(&mut rows)
268 }
269}
270
271fn node_row_id(conn: &Connection, key: &str) -> rusqlite::Result<Option<i64>> {
274 conn.query_row("SELECT id FROM nodes WHERE key = ?1", [key], |r| r.get(0))
275 .optional()
276}
277
278fn upsert_node(conn: &Connection, node: &Node) -> Result<(), StoreError> {
279 let meta = serde_json::to_string(&node.meta)?;
280 let (span_start, span_end) = match node.span {
281 Some(s) => (Some(i64::from(s.start)), Some(i64::from(s.end))),
282 None => (None, None),
283 };
284 conn.execute(
285 "INSERT INTO nodes (key, kind, name, path, lang, blob_hash, span_start, span_end, meta)
286 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
287 ON CONFLICT(key) DO UPDATE SET
288 kind = excluded.kind, name = excluded.name, path = excluded.path,
289 lang = excluded.lang, blob_hash = excluded.blob_hash,
290 span_start = excluded.span_start, span_end = excluded.span_end,
291 meta = excluded.meta",
292 params![
293 node.key,
294 node.kind.as_str(),
295 node.name,
296 node.path,
297 node.lang,
298 node.blob_hash,
299 span_start,
300 span_end,
301 meta,
302 ],
303 )?;
304 Ok(())
305}
306
307fn insert_edge(conn: &Connection, edge: &Edge) -> Result<(), StoreError> {
308 if !edge.is_valid() {
309 return Err(StoreError::InvalidEdge(format!(
310 "confidence must be present iff provenance is inferred (src={}, dst={})",
311 edge.src, edge.dst
312 )));
313 }
314 let src_id =
315 node_row_id(conn, &edge.src)?.ok_or_else(|| StoreError::UnknownNode(edge.src.clone()))?;
316 let dst_id =
317 node_row_id(conn, &edge.dst)?.ok_or_else(|| StoreError::UnknownNode(edge.dst.clone()))?;
318 conn.execute(
323 "INSERT INTO edges (src, dst, kind, provenance, confidence, src_ref)
324 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
325 ON CONFLICT(src, dst, kind, provenance) DO NOTHING",
326 params![
327 src_id,
328 dst_id,
329 edge.kind.as_str(),
330 edge.provenance.as_str(),
331 edge.confidence,
332 edge.src_ref,
333 ],
334 )?;
335 Ok(())
336}
337
338fn collect_nodes(rows: &mut rusqlite::Rows) -> Result<Vec<Node>, StoreError> {
339 let mut out = Vec::new();
340 while let Some(row) = rows.next()? {
341 out.push(row_to_node(row)?);
342 }
343 Ok(out)
344}
345
346fn collect_edges(rows: &mut rusqlite::Rows) -> Result<Vec<Edge>, StoreError> {
347 let mut out = Vec::new();
348 while let Some(row) = rows.next()? {
349 out.push(row_to_edge(row)?);
350 }
351 Ok(out)
352}
353
354fn row_to_node(row: &rusqlite::Row) -> Result<Node, StoreError> {
355 let kind: String = row.get("kind")?;
356 let span_start: Option<i64> = row.get("span_start")?;
357 let span_end: Option<i64> = row.get("span_end")?;
358 let span = match (span_start, span_end) {
359 (Some(s), Some(e)) => Some(Span::new(to_u32(s)?, to_u32(e)?)),
360 _ => None,
361 };
362 let meta: String = row.get("meta")?;
363 Ok(Node {
364 key: row.get("key")?,
365 kind: NodeKind::from_token(&kind),
366 name: row.get("name")?,
367 path: row.get("path")?,
368 lang: row.get("lang")?,
369 blob_hash: row.get("blob_hash")?,
370 span,
371 meta: serde_json::from_str(&meta)?,
372 })
373}
374
375fn row_to_edge(row: &rusqlite::Row) -> Result<Edge, StoreError> {
376 let kind: String = row.get("kind")?;
377 let provenance: String = row.get("provenance")?;
378 let provenance = Provenance::from_token(&provenance)
379 .ok_or_else(|| StoreError::Corrupt(format!("unknown provenance: {provenance}")))?;
380 Ok(Edge {
381 src: row.get("src")?,
382 dst: row.get("dst")?,
383 kind: EdgeKind::from_token(&kind),
384 provenance,
385 confidence: row.get("confidence")?,
386 src_ref: row.get("src_ref")?,
387 })
388}
389
390fn to_u32(v: i64) -> Result<u32, StoreError> {
391 u32::try_from(v).map_err(|_| StoreError::Corrupt(format!("span offset out of range: {v}")))
392}
393
394#[cfg(test)]
395mod tests {
396 use super::Store;
397 use crate::model::{Direction, Edge, EdgeKind, FactSet, Node, NodeKind, Span};
398 use crate::provenance::Provenance;
399
400 fn sample_node(key: &str) -> Node {
401 Node {
402 key: key.to_owned(),
403 kind: NodeKind::Fn,
404 name: "sample".to_owned(),
405 path: Some("src/lib.rs".to_owned()),
406 lang: Some("rust".to_owned()),
407 blob_hash: Some("deadbeef".to_owned()),
408 span: Some(Span::new(10, 42)),
409 meta: serde_json::json!({"vis": "pub"}),
410 }
411 }
412
413 #[test]
414 fn open_in_memory_applies_schema() {
415 let store = Store::open_in_memory().expect("open");
416 assert_eq!(store.node_count().expect("count"), 0);
417 assert_eq!(store.schema_version().expect("version"), 3);
418 }
419
420 #[test]
421 fn upsert_and_get_round_trips_all_fields() {
422 let store = Store::open_in_memory().expect("open");
423 let node = sample_node("sym:rust:src/lib.rs#sample");
424 store.upsert_node(&node).expect("upsert");
425 let got = store.get_node(&node.key).expect("get").expect("present");
426 assert_eq!(got, node);
427 }
428
429 #[test]
430 fn upsert_updates_in_place() {
431 let store = Store::open_in_memory().expect("open");
432 let mut node = sample_node("k");
433 store.upsert_node(&node).expect("insert");
434 node.name = "renamed".to_owned();
435 node.kind = NodeKind::Struct;
436 store.upsert_node(&node).expect("update");
437 assert_eq!(store.node_count().expect("count"), 1);
438 let got = store.get_node("k").expect("get").expect("present");
439 assert_eq!(got.name, "renamed");
440 assert_eq!(got.kind, NodeKind::Struct);
441 }
442
443 #[test]
444 fn edge_with_unknown_endpoint_is_rejected() {
445 let store = Store::open_in_memory().expect("open");
446 store
447 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
448 .expect("a");
449 let edge = Edge::derived("a", "missing", EdgeKind::Calls);
450 let err = store.insert_edge(&edge).expect_err("should reject");
451 assert!(matches!(err, super::StoreError::UnknownNode(k) if k == "missing"));
452 }
453
454 #[test]
455 fn inferred_edge_requires_confidence() {
456 let store = Store::open_in_memory().expect("open");
457 store
458 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
459 .expect("a");
460 store
461 .upsert_node(&Node::new("b", NodeKind::Fn, "b"))
462 .expect("b");
463 let bad = Edge {
465 src: "a".to_owned(),
466 dst: "b".to_owned(),
467 kind: EdgeKind::References,
468 provenance: Provenance::Inferred,
469 confidence: None,
470 src_ref: None,
471 };
472 assert!(matches!(
473 store.insert_edge(&bad).expect_err("reject"),
474 super::StoreError::InvalidEdge(_)
475 ));
476 }
477
478 #[test]
479 fn apply_factset_is_atomic() {
480 let mut store = Store::open_in_memory().expect("open");
481 let facts = FactSet::new()
483 .with_node(Node::new("a", NodeKind::Fn, "a"))
484 .with_node(Node::new("b", NodeKind::Fn, "b"))
485 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
486 .with_edge(Edge::derived("a", "ghost", EdgeKind::Calls));
487 assert!(store.apply_factset(&facts).is_err());
488 assert_eq!(store.node_count().expect("count"), 0, "rolled back");
489 assert_eq!(store.edge_count().expect("count"), 0, "rolled back");
490 }
491
492 #[test]
493 fn neighbors_and_provenance_queries() {
494 let mut store = Store::open_in_memory().expect("open");
495 let facts = FactSet::new()
496 .with_node(Node::new("a", NodeKind::Fn, "a"))
497 .with_node(Node::new("b", NodeKind::Fn, "b"))
498 .with_node(Node::new("c", NodeKind::Fn, "c"))
499 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
500 .with_edge(Edge::inferred("a", "c", EdgeKind::References, 0.5));
501 store.apply_factset(&facts).expect("apply");
502
503 let out = store.neighbors("a", Direction::Outgoing).expect("out");
504 let mut keys: Vec<_> = out.iter().map(|n| n.key.clone()).collect();
505 keys.sort();
506 assert_eq!(keys, ["b", "c"]);
507
508 assert!(
509 store
510 .neighbors("b", Direction::Outgoing)
511 .expect("b out")
512 .is_empty()
513 );
514 assert_eq!(
515 store
516 .neighbors("b", Direction::Incoming)
517 .expect("b in")
518 .len(),
519 1
520 );
521
522 let inferred = store
523 .edges_by_provenance(Provenance::Inferred)
524 .expect("inf");
525 assert_eq!(inferred.len(), 1);
526 assert_eq!(inferred[0].confidence, Some(0.5));
527 }
528
529 #[test]
530 fn neighbors_of_absent_node_is_empty() {
531 let store = Store::open_in_memory().expect("open");
532 assert!(
533 store
534 .neighbors("nope", Direction::Both)
535 .expect("q")
536 .is_empty()
537 );
538 }
539
540 #[test]
541 fn get_missing_node_is_none() {
542 let store = Store::open_in_memory().expect("open");
543 assert!(store.get_node("absent").expect("get").is_none());
544 }
545
546 #[test]
547 fn nodes_by_kind_and_edges_to() {
548 let mut store = Store::open_in_memory().expect("open");
549 let facts = FactSet::new()
550 .with_node(Node::new("f1", NodeKind::Fn, "f1"))
551 .with_node(Node::new("f2", NodeKind::Fn, "f2"))
552 .with_node(Node::new("s1", NodeKind::Struct, "s1"))
553 .with_edge(Edge::derived("f1", "s1", EdgeKind::References))
554 .with_edge(Edge::derived("f2", "s1", EdgeKind::References));
555 store.apply_factset(&facts).expect("apply");
556
557 let fns = store.nodes_by_kind(&NodeKind::Fn).expect("fns");
558 assert_eq!(
559 fns.iter().map(|n| n.key.as_str()).collect::<Vec<_>>(),
560 ["f1", "f2"]
561 );
562 assert!(
563 store
564 .nodes_by_kind(&NodeKind::Enum)
565 .expect("enums")
566 .is_empty()
567 );
568
569 let into_s1 = store.edges_to("s1").expect("edges_to");
570 assert_eq!(into_s1.len(), 2);
571 assert!(into_s1.iter().all(|e| e.dst == "s1"));
572 }
573
574 #[test]
575 fn open_persists_across_reopen() {
576 let path =
577 std::env::temp_dir().join(format!("roteiro-open-test-{}.db", std::process::id()));
578 std::fs::remove_file(&path).ok();
579 {
580 let store = Store::open(&path).expect("open");
581 store
582 .upsert_node(&sample_node("persisted"))
583 .expect("upsert");
584 }
585 {
586 let store = Store::open(&path).expect("reopen");
587 assert_eq!(store.node_count().expect("count"), 1);
588 assert_eq!(store.schema_version().expect("version"), 3);
589 assert!(store.get_node("persisted").expect("get").is_some());
590 }
591 std::fs::remove_file(&path).expect("cleanup");
592 }
593}