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(
319 "INSERT INTO edges (src, dst, kind, provenance, confidence, src_ref)
320 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
321 params![
322 src_id,
323 dst_id,
324 edge.kind.as_str(),
325 edge.provenance.as_str(),
326 edge.confidence,
327 edge.src_ref,
328 ],
329 )?;
330 Ok(())
331}
332
333fn collect_nodes(rows: &mut rusqlite::Rows) -> Result<Vec<Node>, StoreError> {
334 let mut out = Vec::new();
335 while let Some(row) = rows.next()? {
336 out.push(row_to_node(row)?);
337 }
338 Ok(out)
339}
340
341fn collect_edges(rows: &mut rusqlite::Rows) -> Result<Vec<Edge>, StoreError> {
342 let mut out = Vec::new();
343 while let Some(row) = rows.next()? {
344 out.push(row_to_edge(row)?);
345 }
346 Ok(out)
347}
348
349fn row_to_node(row: &rusqlite::Row) -> Result<Node, StoreError> {
350 let kind: String = row.get("kind")?;
351 let span_start: Option<i64> = row.get("span_start")?;
352 let span_end: Option<i64> = row.get("span_end")?;
353 let span = match (span_start, span_end) {
354 (Some(s), Some(e)) => Some(Span::new(to_u32(s)?, to_u32(e)?)),
355 _ => None,
356 };
357 let meta: String = row.get("meta")?;
358 Ok(Node {
359 key: row.get("key")?,
360 kind: NodeKind::from_token(&kind),
361 name: row.get("name")?,
362 path: row.get("path")?,
363 lang: row.get("lang")?,
364 blob_hash: row.get("blob_hash")?,
365 span,
366 meta: serde_json::from_str(&meta)?,
367 })
368}
369
370fn row_to_edge(row: &rusqlite::Row) -> Result<Edge, StoreError> {
371 let kind: String = row.get("kind")?;
372 let provenance: String = row.get("provenance")?;
373 let provenance = Provenance::from_token(&provenance)
374 .ok_or_else(|| StoreError::Corrupt(format!("unknown provenance: {provenance}")))?;
375 Ok(Edge {
376 src: row.get("src")?,
377 dst: row.get("dst")?,
378 kind: EdgeKind::from_token(&kind),
379 provenance,
380 confidence: row.get("confidence")?,
381 src_ref: row.get("src_ref")?,
382 })
383}
384
385fn to_u32(v: i64) -> Result<u32, StoreError> {
386 u32::try_from(v).map_err(|_| StoreError::Corrupt(format!("span offset out of range: {v}")))
387}
388
389#[cfg(test)]
390mod tests {
391 use super::Store;
392 use crate::model::{Direction, Edge, EdgeKind, FactSet, Node, NodeKind, Span};
393 use crate::provenance::Provenance;
394
395 fn sample_node(key: &str) -> Node {
396 Node {
397 key: key.to_owned(),
398 kind: NodeKind::Fn,
399 name: "sample".to_owned(),
400 path: Some("src/lib.rs".to_owned()),
401 lang: Some("rust".to_owned()),
402 blob_hash: Some("deadbeef".to_owned()),
403 span: Some(Span::new(10, 42)),
404 meta: serde_json::json!({"vis": "pub"}),
405 }
406 }
407
408 #[test]
409 fn open_in_memory_applies_schema() {
410 let store = Store::open_in_memory().expect("open");
411 assert_eq!(store.node_count().expect("count"), 0);
412 assert_eq!(store.schema_version().expect("version"), 2);
413 }
414
415 #[test]
416 fn upsert_and_get_round_trips_all_fields() {
417 let store = Store::open_in_memory().expect("open");
418 let node = sample_node("sym:rust:src/lib.rs#sample");
419 store.upsert_node(&node).expect("upsert");
420 let got = store.get_node(&node.key).expect("get").expect("present");
421 assert_eq!(got, node);
422 }
423
424 #[test]
425 fn upsert_updates_in_place() {
426 let store = Store::open_in_memory().expect("open");
427 let mut node = sample_node("k");
428 store.upsert_node(&node).expect("insert");
429 node.name = "renamed".to_owned();
430 node.kind = NodeKind::Struct;
431 store.upsert_node(&node).expect("update");
432 assert_eq!(store.node_count().expect("count"), 1);
433 let got = store.get_node("k").expect("get").expect("present");
434 assert_eq!(got.name, "renamed");
435 assert_eq!(got.kind, NodeKind::Struct);
436 }
437
438 #[test]
439 fn edge_with_unknown_endpoint_is_rejected() {
440 let store = Store::open_in_memory().expect("open");
441 store
442 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
443 .expect("a");
444 let edge = Edge::derived("a", "missing", EdgeKind::Calls);
445 let err = store.insert_edge(&edge).expect_err("should reject");
446 assert!(matches!(err, super::StoreError::UnknownNode(k) if k == "missing"));
447 }
448
449 #[test]
450 fn inferred_edge_requires_confidence() {
451 let store = Store::open_in_memory().expect("open");
452 store
453 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
454 .expect("a");
455 store
456 .upsert_node(&Node::new("b", NodeKind::Fn, "b"))
457 .expect("b");
458 let bad = Edge {
460 src: "a".to_owned(),
461 dst: "b".to_owned(),
462 kind: EdgeKind::References,
463 provenance: Provenance::Inferred,
464 confidence: None,
465 src_ref: None,
466 };
467 assert!(matches!(
468 store.insert_edge(&bad).expect_err("reject"),
469 super::StoreError::InvalidEdge(_)
470 ));
471 }
472
473 #[test]
474 fn apply_factset_is_atomic() {
475 let mut store = Store::open_in_memory().expect("open");
476 let facts = FactSet::new()
478 .with_node(Node::new("a", NodeKind::Fn, "a"))
479 .with_node(Node::new("b", NodeKind::Fn, "b"))
480 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
481 .with_edge(Edge::derived("a", "ghost", EdgeKind::Calls));
482 assert!(store.apply_factset(&facts).is_err());
483 assert_eq!(store.node_count().expect("count"), 0, "rolled back");
484 assert_eq!(store.edge_count().expect("count"), 0, "rolled back");
485 }
486
487 #[test]
488 fn neighbors_and_provenance_queries() {
489 let mut store = Store::open_in_memory().expect("open");
490 let facts = FactSet::new()
491 .with_node(Node::new("a", NodeKind::Fn, "a"))
492 .with_node(Node::new("b", NodeKind::Fn, "b"))
493 .with_node(Node::new("c", NodeKind::Fn, "c"))
494 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
495 .with_edge(Edge::inferred("a", "c", EdgeKind::References, 0.5));
496 store.apply_factset(&facts).expect("apply");
497
498 let out = store.neighbors("a", Direction::Outgoing).expect("out");
499 let mut keys: Vec<_> = out.iter().map(|n| n.key.clone()).collect();
500 keys.sort();
501 assert_eq!(keys, ["b", "c"]);
502
503 assert!(
504 store
505 .neighbors("b", Direction::Outgoing)
506 .expect("b out")
507 .is_empty()
508 );
509 assert_eq!(
510 store
511 .neighbors("b", Direction::Incoming)
512 .expect("b in")
513 .len(),
514 1
515 );
516
517 let inferred = store
518 .edges_by_provenance(Provenance::Inferred)
519 .expect("inf");
520 assert_eq!(inferred.len(), 1);
521 assert_eq!(inferred[0].confidence, Some(0.5));
522 }
523
524 #[test]
525 fn neighbors_of_absent_node_is_empty() {
526 let store = Store::open_in_memory().expect("open");
527 assert!(
528 store
529 .neighbors("nope", Direction::Both)
530 .expect("q")
531 .is_empty()
532 );
533 }
534
535 #[test]
536 fn get_missing_node_is_none() {
537 let store = Store::open_in_memory().expect("open");
538 assert!(store.get_node("absent").expect("get").is_none());
539 }
540
541 #[test]
542 fn nodes_by_kind_and_edges_to() {
543 let mut store = Store::open_in_memory().expect("open");
544 let facts = FactSet::new()
545 .with_node(Node::new("f1", NodeKind::Fn, "f1"))
546 .with_node(Node::new("f2", NodeKind::Fn, "f2"))
547 .with_node(Node::new("s1", NodeKind::Struct, "s1"))
548 .with_edge(Edge::derived("f1", "s1", EdgeKind::References))
549 .with_edge(Edge::derived("f2", "s1", EdgeKind::References));
550 store.apply_factset(&facts).expect("apply");
551
552 let fns = store.nodes_by_kind(&NodeKind::Fn).expect("fns");
553 assert_eq!(
554 fns.iter().map(|n| n.key.as_str()).collect::<Vec<_>>(),
555 ["f1", "f2"]
556 );
557 assert!(
558 store
559 .nodes_by_kind(&NodeKind::Enum)
560 .expect("enums")
561 .is_empty()
562 );
563
564 let into_s1 = store.edges_to("s1").expect("edges_to");
565 assert_eq!(into_s1.len(), 2);
566 assert!(into_s1.iter().all(|e| e.dst == "s1"));
567 }
568
569 #[test]
570 fn open_persists_across_reopen() {
571 let path =
572 std::env::temp_dir().join(format!("roteiro-open-test-{}.db", std::process::id()));
573 std::fs::remove_file(&path).ok();
574 {
575 let store = Store::open(&path).expect("open");
576 store
577 .upsert_node(&sample_node("persisted"))
578 .expect("upsert");
579 }
580 {
581 let store = Store::open(&path).expect("reopen");
582 assert_eq!(store.node_count().expect("count"), 1);
583 assert_eq!(store.schema_version().expect("version"), 2);
584 assert!(store.get_node("persisted").expect("get").is_some());
585 }
586 std::fs::remove_file(&path).expect("cleanup");
587 }
588}