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
31#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
34pub struct ImportApplied {
35 pub layers: usize,
37 pub nodes: usize,
39 pub edges_applied: usize,
42 pub edges_pruned: usize,
46}
47
48const NODE_COLS: &str =
50 "n.key, n.kind, n.name, n.path, n.lang, n.blob_hash, n.span_start, n.span_end, n.meta";
51
52const EDGE_SELECT: &str = "SELECT ns.key AS src, nd.key AS dst, e.kind, e.provenance, \
54 e.confidence, e.src_ref \
55 FROM edges e JOIN nodes ns ON ns.id = e.src JOIN nodes nd ON nd.id = e.dst";
56
57pub struct Store {
59 conn: Connection,
60}
61
62impl Store {
63 pub fn open(path: &Path) -> Result<Self, StoreError> {
69 let conn = Connection::open(path)?;
70 Self::from_conn(conn)
71 }
72
73 pub fn open_in_memory() -> Result<Self, StoreError> {
78 let conn = Connection::open_in_memory()?;
79 Self::from_conn(conn)
80 }
81
82 fn from_conn(mut conn: Connection) -> Result<Self, StoreError> {
83 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
84 migrations::apply(&mut conn)?;
85 Ok(Self { conn })
86 }
87
88 pub fn schema_version(&self) -> Result<u32, StoreError> {
93 let v: i64 = self.conn.query_row(
94 "SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
95 [],
96 |r| r.get(0),
97 )?;
98 Ok(u32::try_from(v).unwrap_or(0))
99 }
100
101 pub fn node_count(&self) -> Result<u64, StoreError> {
106 let n: i64 = self
107 .conn
108 .query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))?;
109 Ok(u64::try_from(n).unwrap_or(0))
110 }
111
112 pub fn edge_count(&self) -> Result<u64, StoreError> {
117 let n: i64 = self
118 .conn
119 .query_row("SELECT COUNT(*) FROM edges", [], |r| r.get(0))?;
120 Ok(u64::try_from(n).unwrap_or(0))
121 }
122
123 pub fn upsert_node(&self, node: &Node) -> Result<(), StoreError> {
129 upsert_node(&self.conn, node)
130 }
131
132 pub fn insert_edge(&self, edge: &Edge) -> Result<(), StoreError> {
139 insert_edge(&self.conn, edge)
140 }
141
142 pub fn apply_factset(&mut self, facts: &FactSet) -> Result<(), StoreError> {
149 let tx = self.conn.transaction()?;
150 for node in &facts.nodes {
151 upsert_node(&tx, node)?;
152 }
153 for edge in &facts.edges {
154 insert_edge(&tx, edge)?;
155 }
156 tx.commit()?;
157 Ok(())
158 }
159
160 pub fn sync_state(&self) -> Result<Option<String>, StoreError> {
166 Ok(self
167 .conn
168 .query_row("SELECT tree FROM sync_state WHERE id = 0", [], |r| r.get(0))
169 .optional()?)
170 }
171
172 pub fn rebuild(&mut self, facts: &FactSet, tree: Option<&str>) -> Result<(), StoreError> {
185 let tx = self.conn.transaction()?;
186 tx.execute("DELETE FROM edges", [])?;
187 tx.execute("DELETE FROM nodes", [])?;
188 for node in &facts.nodes {
189 upsert_node(&tx, node)?;
190 }
191 for edge in &facts.edges {
192 insert_edge(&tx, edge)?;
193 }
194 match tree {
195 Some(tree) => tx.execute(
196 "INSERT INTO sync_state (id, tree) VALUES (0, ?1)
197 ON CONFLICT(id) DO UPDATE SET tree = excluded.tree",
198 [tree],
199 )?,
200 None => tx.execute("DELETE FROM sync_state WHERE id = 0", [])?,
201 };
202 tx.commit()?;
203 Ok(())
204 }
205
206 pub fn get_node(&self, key: &str) -> Result<Option<Node>, StoreError> {
212 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.key = ?1");
213 let mut stmt = self.conn.prepare(&sql)?;
214 let mut rows = stmt.query([key])?;
215 match rows.next()? {
216 Some(row) => Ok(Some(row_to_node(row)?)),
217 None => Ok(None),
218 }
219 }
220
221 pub fn all_keys(&self) -> Result<Vec<String>, StoreError> {
226 let mut stmt = self.conn.prepare("SELECT key FROM nodes ORDER BY key")?;
227 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
228 let mut out = Vec::new();
229 for row in rows {
230 out.push(row?);
231 }
232 Ok(out)
233 }
234
235 pub fn export_factset(&self) -> Result<FactSet, StoreError> {
242 let node_sql = format!("SELECT {NODE_COLS} FROM nodes n ORDER BY n.key");
243 let mut node_stmt = self.conn.prepare(&node_sql)?;
244 let mut node_rows = node_stmt.query([])?;
245 let nodes = collect_nodes(&mut node_rows)?;
246
247 let edge_sql = format!("{EDGE_SELECT} ORDER BY ns.key, nd.key, e.kind, e.provenance");
250 let mut edge_stmt = self.conn.prepare(&edge_sql)?;
251 let mut edge_rows = edge_stmt.query([])?;
252 let edges = collect_edges(&mut edge_rows)?;
253
254 Ok(FactSet { nodes, edges })
255 }
256
257 pub fn nodes_by_kind(&self, kind: &NodeKind) -> Result<Vec<Node>, StoreError> {
263 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.kind = ?1 ORDER BY n.key");
264 let mut stmt = self.conn.prepare(&sql)?;
265 let mut rows = stmt.query([kind.as_str()])?;
266 collect_nodes(&mut rows)
267 }
268
269 pub fn all_nodes(&self) -> Result<Vec<Node>, StoreError> {
276 let sql = format!("SELECT {NODE_COLS} FROM nodes n ORDER BY n.key");
277 let mut stmt = self.conn.prepare(&sql)?;
278 let mut rows = stmt.query([])?;
279 collect_nodes(&mut rows)
280 }
281
282 pub fn edges_from(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
287 let sql = format!("{EDGE_SELECT} WHERE ns.key = ?1 ORDER BY e.id");
288 let mut stmt = self.conn.prepare(&sql)?;
289 let mut rows = stmt.query([key])?;
290 collect_edges(&mut rows)
291 }
292
293 pub fn edges_to(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
298 let sql = format!("{EDGE_SELECT} WHERE nd.key = ?1 ORDER BY e.id");
299 let mut stmt = self.conn.prepare(&sql)?;
300 let mut rows = stmt.query([key])?;
301 collect_edges(&mut rows)
302 }
303
304 pub fn edges_by_provenance(&self, provenance: Provenance) -> Result<Vec<Edge>, StoreError> {
309 let sql = format!("{EDGE_SELECT} WHERE e.provenance = ?1 ORDER BY e.id");
310 let mut stmt = self.conn.prepare(&sql)?;
311 let mut rows = stmt.query([provenance.as_str()])?;
312 collect_edges(&mut rows)
313 }
314
315 pub fn delete_edges_by_provenance(&self, provenance: Provenance) -> Result<u64, StoreError> {
322 let n = self.conn.execute(
323 "DELETE FROM edges WHERE provenance = ?1",
324 [provenance.as_str()],
325 )?;
326 Ok(u64::try_from(n).unwrap_or(0))
327 }
328
329 pub fn delete_edges_by_src_ref(&self, src_ref: &str) -> Result<u64, StoreError> {
337 let n = self
338 .conn
339 .execute("DELETE FROM edges WHERE src_ref = ?1", [src_ref])?;
340 Ok(u64::try_from(n).unwrap_or(0))
341 }
342
343 pub fn apply_import_layer(
358 &mut self,
359 src_ref: &str,
360 facts: &FactSet,
361 ) -> Result<ImportApplied, StoreError> {
362 let tx = self.conn.transaction()?;
363 tx.execute("DELETE FROM edges WHERE src_ref = ?1", [src_ref])?;
365 for node in &facts.nodes {
366 upsert_node(&tx, node)?;
367 }
368 let (kept, applied) = apply_edges_pruning(&tx, &facts.edges)?;
369 let trimmed = FactSet {
370 nodes: facts.nodes.clone(),
371 edges: kept,
372 };
373 put_import_row(&tx, src_ref, &trimmed)?;
374 tx.commit()?;
375 Ok(ImportApplied {
376 layers: 1,
377 nodes: facts.nodes.len(),
378 ..applied
379 })
380 }
381
382 pub fn delete_import(&self, src_ref: &str) -> Result<bool, StoreError> {
389 let n = self
390 .conn
391 .execute("DELETE FROM imports WHERE src_ref = ?1", [src_ref])?;
392 Ok(n > 0)
393 }
394
395 pub fn import_refs(&self) -> Result<Vec<String>, StoreError> {
400 let mut stmt = self
401 .conn
402 .prepare("SELECT src_ref FROM imports ORDER BY src_ref")?;
403 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
404 let mut out = Vec::new();
405 for row in rows {
406 out.push(row?);
407 }
408 Ok(out)
409 }
410
411 pub fn reapply_imports(&mut self) -> Result<ImportApplied, StoreError> {
422 let layers = self.load_import_layers()?;
423 let tx = self.conn.transaction()?;
424 for (_, facts) in &layers {
427 for node in &facts.nodes {
428 upsert_node(&tx, node)?;
429 }
430 }
431 let mut applied = ImportApplied {
433 layers: layers.len(),
434 ..ImportApplied::default()
435 };
436 for (src_ref, facts) in &layers {
437 applied.nodes += facts.nodes.len();
438 let (kept, counts) = apply_edges_pruning(&tx, &facts.edges)?;
439 applied.edges_applied += counts.edges_applied;
440 applied.edges_pruned += counts.edges_pruned;
441 if kept.len() != facts.edges.len() {
442 let trimmed = FactSet {
443 nodes: facts.nodes.clone(),
444 edges: kept,
445 };
446 put_import_row(&tx, src_ref, &trimmed)?;
447 }
448 }
449 tx.commit()?;
450 Ok(applied)
451 }
452
453 fn load_import_layers(&self) -> Result<Vec<(String, FactSet)>, StoreError> {
456 let mut stmt = self
457 .conn
458 .prepare("SELECT src_ref, facts FROM imports ORDER BY src_ref")?;
459 let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?;
460 let mut out = Vec::new();
461 for row in rows {
462 let (src_ref, json) = row?;
463 out.push((src_ref, serde_json::from_str::<FactSet>(&json)?));
464 }
465 Ok(out)
466 }
467
468 pub fn neighbors(&self, key: &str, dir: Direction) -> Result<Vec<Node>, StoreError> {
475 let out = format!(
476 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.dst \
477 JOIN nodes s ON s.id = e.src WHERE s.key = ?1"
478 );
479 let inc = format!(
480 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.src \
481 JOIN nodes d ON d.id = e.dst WHERE d.key = ?1"
482 );
483 let sql = match dir {
488 Direction::Outgoing => format!("{out} ORDER BY 1"),
489 Direction::Incoming => format!("{inc} ORDER BY 1"),
490 Direction::Both => format!("{out} UNION {inc} ORDER BY 1"),
491 };
492 let mut stmt = self.conn.prepare(&sql)?;
493 let mut rows = stmt.query([key])?;
494 collect_nodes(&mut rows)
495 }
496
497 pub fn context_cache_get(&self, key: &str) -> Result<Option<(String, String)>, StoreError> {
504 let row = self
505 .conn
506 .query_row(
507 "SELECT fingerprint, json FROM node_context WHERE key = ?1",
508 [key],
509 |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
510 )
511 .optional()?;
512 Ok(row)
513 }
514
515 pub fn context_cache_fingerprint(&self, key: &str) -> Result<Option<String>, StoreError> {
521 let fp = self
522 .conn
523 .query_row(
524 "SELECT fingerprint FROM node_context WHERE key = ?1",
525 [key],
526 |r| r.get::<_, String>(0),
527 )
528 .optional()?;
529 Ok(fp)
530 }
531
532 pub fn context_cache_put(
537 &self,
538 key: &str,
539 fingerprint: &str,
540 json: &str,
541 ) -> Result<(), StoreError> {
542 self.conn.execute(
543 "INSERT INTO node_context (key, fingerprint, json) VALUES (?1, ?2, ?3)
544 ON CONFLICT(key) DO UPDATE SET
545 fingerprint = excluded.fingerprint, json = excluded.json",
546 [key, fingerprint, json],
547 )?;
548 Ok(())
549 }
550
551 pub fn context_cache_delete(&self, key: &str) -> Result<bool, StoreError> {
556 let n = self
557 .conn
558 .execute("DELETE FROM node_context WHERE key = ?1", [key])?;
559 Ok(n > 0)
560 }
561
562 pub fn context_cache_keys(&self) -> Result<Vec<String>, StoreError> {
568 let mut stmt = self
569 .conn
570 .prepare("SELECT key FROM node_context ORDER BY key")?;
571 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
572 let mut out = Vec::new();
573 for row in rows {
574 out.push(row?);
575 }
576 Ok(out)
577 }
578}
579
580fn node_row_id(conn: &Connection, key: &str) -> rusqlite::Result<Option<i64>> {
583 conn.query_row("SELECT id FROM nodes WHERE key = ?1", [key], |r| r.get(0))
584 .optional()
585}
586
587fn upsert_node(conn: &Connection, node: &Node) -> Result<(), StoreError> {
588 let meta = serde_json::to_string(&node.meta)?;
589 let (span_start, span_end) = match node.span {
590 Some(s) => (Some(i64::from(s.start)), Some(i64::from(s.end))),
591 None => (None, None),
592 };
593 conn.execute(
594 "INSERT INTO nodes (key, kind, name, path, lang, blob_hash, span_start, span_end, meta)
595 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
596 ON CONFLICT(key) DO UPDATE SET
597 kind = excluded.kind, name = excluded.name, path = excluded.path,
598 lang = excluded.lang, blob_hash = excluded.blob_hash,
599 span_start = excluded.span_start, span_end = excluded.span_end,
600 meta = excluded.meta",
601 params![
602 node.key,
603 node.kind.as_str(),
604 node.name,
605 node.path,
606 node.lang,
607 node.blob_hash,
608 span_start,
609 span_end,
610 meta,
611 ],
612 )?;
613 Ok(())
614}
615
616fn insert_edge(conn: &Connection, edge: &Edge) -> Result<(), StoreError> {
617 validate_edge(edge)?;
618 let src_id =
619 node_row_id(conn, &edge.src)?.ok_or_else(|| StoreError::UnknownNode(edge.src.clone()))?;
620 let dst_id =
621 node_row_id(conn, &edge.dst)?.ok_or_else(|| StoreError::UnknownNode(edge.dst.clone()))?;
622 insert_edge_row(conn, edge, src_id, dst_id)
623}
624
625fn insert_edge_if_present(conn: &Connection, edge: &Edge) -> Result<bool, StoreError> {
631 validate_edge(edge)?;
632 let (Some(src_id), Some(dst_id)) =
633 (node_row_id(conn, &edge.src)?, node_row_id(conn, &edge.dst)?)
634 else {
635 return Ok(false);
636 };
637 insert_edge_row(conn, edge, src_id, dst_id)?;
638 Ok(true)
639}
640
641fn apply_edges_pruning(
645 conn: &Connection,
646 edges: &[Edge],
647) -> Result<(Vec<Edge>, ImportApplied), StoreError> {
648 let mut kept = Vec::with_capacity(edges.len());
649 let mut counts = ImportApplied::default();
650 for edge in edges {
651 if insert_edge_if_present(conn, edge)? {
652 kept.push(edge.clone());
653 counts.edges_applied += 1;
654 } else {
655 counts.edges_pruned += 1;
656 }
657 }
658 Ok((kept, counts))
659}
660
661fn put_import_row(conn: &Connection, src_ref: &str, facts: &FactSet) -> Result<(), StoreError> {
664 let json = serde_json::to_string(facts)?;
665 conn.execute(
666 "INSERT INTO imports (src_ref, facts) VALUES (?1, ?2)
667 ON CONFLICT(src_ref) DO UPDATE SET facts = excluded.facts, imported_at = datetime('now')",
668 params![src_ref, json],
669 )?;
670 Ok(())
671}
672
673fn validate_edge(edge: &Edge) -> Result<(), StoreError> {
676 if edge.is_valid() {
677 Ok(())
678 } else {
679 Err(StoreError::InvalidEdge(format!(
680 "confidence must be present iff provenance is inferred (src={}, dst={})",
681 edge.src, edge.dst
682 )))
683 }
684}
685
686fn insert_edge_row(
691 conn: &Connection,
692 edge: &Edge,
693 src_id: i64,
694 dst_id: i64,
695) -> Result<(), StoreError> {
696 conn.execute(
697 "INSERT INTO edges (src, dst, kind, provenance, confidence, src_ref)
698 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
699 ON CONFLICT(src, dst, kind, provenance) DO NOTHING",
700 params![
701 src_id,
702 dst_id,
703 edge.kind.as_str(),
704 edge.provenance.as_str(),
705 edge.confidence,
706 edge.src_ref,
707 ],
708 )?;
709 Ok(())
710}
711
712fn collect_nodes(rows: &mut rusqlite::Rows) -> Result<Vec<Node>, StoreError> {
713 let mut out = Vec::new();
714 while let Some(row) = rows.next()? {
715 out.push(row_to_node(row)?);
716 }
717 Ok(out)
718}
719
720fn collect_edges(rows: &mut rusqlite::Rows) -> Result<Vec<Edge>, StoreError> {
721 let mut out = Vec::new();
722 while let Some(row) = rows.next()? {
723 out.push(row_to_edge(row)?);
724 }
725 Ok(out)
726}
727
728fn row_to_node(row: &rusqlite::Row) -> Result<Node, StoreError> {
729 let kind: String = row.get("kind")?;
730 let span_start: Option<i64> = row.get("span_start")?;
731 let span_end: Option<i64> = row.get("span_end")?;
732 let span = match (span_start, span_end) {
733 (Some(s), Some(e)) => Some(Span::new(to_u32(s)?, to_u32(e)?)),
734 _ => None,
735 };
736 let meta: String = row.get("meta")?;
737 Ok(Node {
738 key: row.get("key")?,
739 kind: NodeKind::from_token(&kind),
740 name: row.get("name")?,
741 path: row.get("path")?,
742 lang: row.get("lang")?,
743 blob_hash: row.get("blob_hash")?,
744 span,
745 meta: serde_json::from_str(&meta)?,
746 })
747}
748
749fn row_to_edge(row: &rusqlite::Row) -> Result<Edge, StoreError> {
750 let kind: String = row.get("kind")?;
751 let provenance: String = row.get("provenance")?;
752 let provenance = Provenance::from_token(&provenance)
753 .ok_or_else(|| StoreError::Corrupt(format!("unknown provenance: {provenance}")))?;
754 Ok(Edge {
755 src: row.get("src")?,
756 dst: row.get("dst")?,
757 kind: EdgeKind::from_token(&kind),
758 provenance,
759 confidence: row.get("confidence")?,
760 src_ref: row.get("src_ref")?,
761 })
762}
763
764fn to_u32(v: i64) -> Result<u32, StoreError> {
765 u32::try_from(v).map_err(|_| StoreError::Corrupt(format!("span offset out of range: {v}")))
766}
767
768#[cfg(test)]
769mod tests {
770 use super::Store;
771 use crate::model::{Direction, Edge, EdgeKind, FactSet, Node, NodeKind, Span};
772 use crate::provenance::Provenance;
773
774 fn sample_node(key: &str) -> Node {
775 Node {
776 key: key.to_owned(),
777 kind: NodeKind::Fn,
778 name: "sample".to_owned(),
779 path: Some("src/lib.rs".to_owned()),
780 lang: Some("rust".to_owned()),
781 blob_hash: Some("deadbeef".to_owned()),
782 span: Some(Span::new(10, 42)),
783 meta: serde_json::json!({"vis": "pub"}),
784 }
785 }
786
787 #[test]
788 fn open_in_memory_applies_schema() {
789 let store = Store::open_in_memory().expect("open");
790 assert_eq!(store.node_count().expect("count"), 0);
791 assert_eq!(store.schema_version().expect("version"), 5);
792 }
793
794 #[test]
795 fn upsert_and_get_round_trips_all_fields() {
796 let store = Store::open_in_memory().expect("open");
797 let node = sample_node("sym:rust:src/lib.rs#sample");
798 store.upsert_node(&node).expect("upsert");
799 let got = store.get_node(&node.key).expect("get").expect("present");
800 assert_eq!(got, node);
801 }
802
803 #[test]
804 fn upsert_updates_in_place() {
805 let store = Store::open_in_memory().expect("open");
806 let mut node = sample_node("k");
807 store.upsert_node(&node).expect("insert");
808 node.name = "renamed".to_owned();
809 node.kind = NodeKind::Struct;
810 store.upsert_node(&node).expect("update");
811 assert_eq!(store.node_count().expect("count"), 1);
812 let got = store.get_node("k").expect("get").expect("present");
813 assert_eq!(got.name, "renamed");
814 assert_eq!(got.kind, NodeKind::Struct);
815 }
816
817 #[test]
818 fn edge_with_unknown_endpoint_is_rejected() {
819 let store = Store::open_in_memory().expect("open");
820 store
821 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
822 .expect("a");
823 let edge = Edge::derived("a", "missing", EdgeKind::Calls);
824 let err = store.insert_edge(&edge).expect_err("should reject");
825 assert!(matches!(err, super::StoreError::UnknownNode(k) if k == "missing"));
826 }
827
828 #[test]
829 fn inferred_edge_requires_confidence() {
830 let store = Store::open_in_memory().expect("open");
831 store
832 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
833 .expect("a");
834 store
835 .upsert_node(&Node::new("b", NodeKind::Fn, "b"))
836 .expect("b");
837 let bad = Edge {
839 src: "a".to_owned(),
840 dst: "b".to_owned(),
841 kind: EdgeKind::References,
842 provenance: Provenance::Inferred,
843 confidence: None,
844 src_ref: None,
845 };
846 assert!(matches!(
847 store.insert_edge(&bad).expect_err("reject"),
848 super::StoreError::InvalidEdge(_)
849 ));
850 }
851
852 #[test]
853 fn apply_factset_is_atomic() {
854 let mut store = Store::open_in_memory().expect("open");
855 let facts = FactSet::new()
857 .with_node(Node::new("a", NodeKind::Fn, "a"))
858 .with_node(Node::new("b", NodeKind::Fn, "b"))
859 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
860 .with_edge(Edge::derived("a", "ghost", EdgeKind::Calls));
861 assert!(store.apply_factset(&facts).is_err());
862 assert_eq!(store.node_count().expect("count"), 0, "rolled back");
863 assert_eq!(store.edge_count().expect("count"), 0, "rolled back");
864 }
865
866 #[test]
867 fn neighbors_and_provenance_queries() {
868 let mut store = Store::open_in_memory().expect("open");
869 let facts = FactSet::new()
870 .with_node(Node::new("a", NodeKind::Fn, "a"))
871 .with_node(Node::new("b", NodeKind::Fn, "b"))
872 .with_node(Node::new("c", NodeKind::Fn, "c"))
873 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
874 .with_edge(Edge::inferred("a", "c", EdgeKind::References, 0.5));
875 store.apply_factset(&facts).expect("apply");
876
877 let out = store.neighbors("a", Direction::Outgoing).expect("out");
878 let mut keys: Vec<_> = out.iter().map(|n| n.key.clone()).collect();
879 keys.sort();
880 assert_eq!(keys, ["b", "c"]);
881
882 assert!(
883 store
884 .neighbors("b", Direction::Outgoing)
885 .expect("b out")
886 .is_empty()
887 );
888 assert_eq!(
889 store
890 .neighbors("b", Direction::Incoming)
891 .expect("b in")
892 .len(),
893 1
894 );
895
896 let inferred = store
897 .edges_by_provenance(Provenance::Inferred)
898 .expect("inf");
899 assert_eq!(inferred.len(), 1);
900 assert_eq!(inferred[0].confidence, Some(0.5));
901 }
902
903 #[test]
904 fn neighbors_of_absent_node_is_empty() {
905 let store = Store::open_in_memory().expect("open");
906 assert!(
907 store
908 .neighbors("nope", Direction::Both)
909 .expect("q")
910 .is_empty()
911 );
912 }
913
914 #[test]
915 fn get_missing_node_is_none() {
916 let store = Store::open_in_memory().expect("open");
917 assert!(store.get_node("absent").expect("get").is_none());
918 }
919
920 #[test]
921 fn nodes_by_kind_and_edges_to() {
922 let mut store = Store::open_in_memory().expect("open");
923 let facts = FactSet::new()
924 .with_node(Node::new("f1", NodeKind::Fn, "f1"))
925 .with_node(Node::new("f2", NodeKind::Fn, "f2"))
926 .with_node(Node::new("s1", NodeKind::Struct, "s1"))
927 .with_edge(Edge::derived("f1", "s1", EdgeKind::References))
928 .with_edge(Edge::derived("f2", "s1", EdgeKind::References));
929 store.apply_factset(&facts).expect("apply");
930
931 let fns = store.nodes_by_kind(&NodeKind::Fn).expect("fns");
932 assert_eq!(
933 fns.iter().map(|n| n.key.as_str()).collect::<Vec<_>>(),
934 ["f1", "f2"]
935 );
936 assert!(
937 store
938 .nodes_by_kind(&NodeKind::Enum)
939 .expect("enums")
940 .is_empty()
941 );
942
943 let into_s1 = store.edges_to("s1").expect("edges_to");
944 assert_eq!(into_s1.len(), 2);
945 assert!(into_s1.iter().all(|e| e.dst == "s1"));
946 }
947
948 #[test]
949 fn open_persists_across_reopen() {
950 let path =
951 std::env::temp_dir().join(format!("roteiro-open-test-{}.db", std::process::id()));
952 std::fs::remove_file(&path).ok();
953 {
954 let store = Store::open(&path).expect("open");
955 store
956 .upsert_node(&sample_node("persisted"))
957 .expect("upsert");
958 }
959 {
960 let store = Store::open(&path).expect("reopen");
961 assert_eq!(store.node_count().expect("count"), 1);
962 assert_eq!(store.schema_version().expect("version"), 5);
963 assert!(store.get_node("persisted").expect("get").is_some());
964 }
965 std::fs::remove_file(&path).expect("cleanup");
966 }
967
968 fn graphify_layer(dst: &str) -> FactSet {
969 FactSet::new()
970 .with_node(Node::new("graphify:doc1", NodeKind::Doc, "Doc 1"))
971 .with_edge({
972 let mut e = Edge::inferred("graphify:doc1", dst, EdgeKind::References, 0.9);
973 e.src_ref = Some("import:graphify".to_owned());
974 e
975 })
976 }
977
978 #[test]
981 fn imports_survive_rebuild() {
982 let mut store = Store::open_in_memory().expect("open");
983 let derived = FactSet::new().with_node(Node::new("file:a.rs", NodeKind::File, "a.rs"));
984 store.rebuild(&derived, Some("tree1")).expect("rebuild");
985
986 let applied = store
988 .apply_import_layer("import:graphify", &graphify_layer("file:a.rs"))
989 .expect("apply import");
990 assert_eq!(applied.edges_applied, 1);
991 assert_eq!(applied.edges_pruned, 0);
992 assert_eq!(store.import_refs().expect("refs"), vec!["import:graphify"]);
993
994 store.rebuild(&derived, Some("tree2")).expect("rebuild2");
997 assert!(store.get_node("graphify:doc1").expect("get").is_none());
998
999 let applied = store.reapply_imports().expect("reapply");
1001 assert_eq!(applied.layers, 1);
1002 assert_eq!(applied.nodes, 1);
1003 assert_eq!(applied.edges_applied, 1);
1004 assert_eq!(applied.edges_pruned, 0);
1005 assert!(store.get_node("graphify:doc1").expect("get").is_some());
1006 assert_eq!(store.edges_from("graphify:doc1").expect("edges").len(), 1);
1007 }
1008
1009 #[test]
1013 fn reapply_prunes_stale_cross_references() {
1014 let mut store = Store::open_in_memory().expect("open");
1015 let derived = FactSet::new().with_node(Node::new("file:gone.rs", NodeKind::File, "g"));
1016 store.rebuild(&derived, Some("t1")).expect("rebuild");
1017 store
1018 .apply_import_layer("import:graphify", &graphify_layer("file:gone.rs"))
1019 .expect("import");
1020
1021 store
1023 .rebuild(&FactSet::new(), Some("t2"))
1024 .expect("rebuild2");
1025 let applied = store.reapply_imports().expect("reapply");
1026 assert_eq!(applied.nodes, 1);
1027 assert_eq!(applied.edges_applied, 0);
1028 assert_eq!(
1029 applied.edges_pruned, 1,
1030 "the edge to the deleted file is pruned"
1031 );
1032 assert!(store.get_node("graphify:doc1").expect("get").is_some());
1033
1034 let again = store.reapply_imports().expect("reapply2");
1037 assert_eq!(again.edges_applied, 0);
1038 assert_eq!(again.edges_pruned, 0, "already pruned; not retried");
1039 }
1040
1041 #[test]
1044 fn apply_import_layer_prunes_on_import() {
1045 let mut store = Store::open_in_memory().expect("open");
1046 let present = || FactSet::new().with_node(Node::new("file:a.rs", NodeKind::File, "a"));
1047 store.rebuild(&present(), Some("t")).expect("rebuild");
1048
1049 let layer = graphify_layer("file:a.rs").with_edge({
1050 let mut e = Edge::inferred("graphify:doc1", "file:ghost.rs", EdgeKind::References, 0.9);
1052 e.src_ref = Some("import:graphify".to_owned());
1053 e
1054 });
1055 let applied = store
1056 .apply_import_layer("import:graphify", &layer)
1057 .expect("import");
1058 assert_eq!(applied.edges_applied, 1);
1059 assert_eq!(applied.edges_pruned, 1);
1060
1061 store.rebuild(&present(), Some("t2")).expect("rebuild2");
1063 let re = store.reapply_imports().expect("reapply");
1064 assert_eq!(re.edges_applied, 1);
1065 assert_eq!(re.edges_pruned, 0, "ghost edge was not persisted");
1066 }
1067
1068 #[test]
1070 fn apply_import_replaces_and_delete_removes() {
1071 let mut store = Store::open_in_memory().expect("open");
1072 let a = FactSet::new().with_node(Node::new("graphify:x", NodeKind::Doc, "x"));
1073 let b = FactSet::new().with_node(Node::new("graphify:y", NodeKind::Doc, "y"));
1074 store.apply_import_layer("import:graphify", &a).expect("a");
1075 store.apply_import_layer("import:graphify", &b).expect("b");
1076 assert_eq!(
1077 store.import_refs().expect("refs").len(),
1078 1,
1079 "same ref replaced"
1080 );
1081 assert!(store.delete_import("import:graphify").expect("del"));
1082 assert!(store.import_refs().expect("refs").is_empty());
1083 assert!(!store.delete_import("import:graphify").expect("del again"));
1084 }
1085}