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 = "n.key, n.kind, n.name, n.path, n.lang, n.blob_hash, n.span_start, n.span_end, n.provenance, n.meta";
50
51const EDGE_SELECT: &str = "SELECT ns.key AS src, nd.key AS dst, e.kind, e.provenance, \
53 e.confidence, e.src_ref \
54 FROM edges e JOIN nodes ns ON ns.id = e.src JOIN nodes nd ON nd.id = e.dst";
55
56pub struct Store {
58 conn: Connection,
59}
60
61impl Store {
62 pub fn open(path: &Path) -> Result<Self, StoreError> {
68 let conn = Connection::open(path)?;
69 Self::from_conn(conn)
70 }
71
72 pub fn open_in_memory() -> Result<Self, StoreError> {
77 let conn = Connection::open_in_memory()?;
78 Self::from_conn(conn)
79 }
80
81 fn from_conn(mut conn: Connection) -> Result<Self, StoreError> {
82 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
83 conn.busy_timeout(std::time::Duration::from_secs(5))?;
89 migrations::apply(&mut conn)?;
90 Ok(Self { conn })
91 }
92
93 pub fn schema_version(&self) -> Result<u32, StoreError> {
98 let v: i64 = self.conn.query_row(
99 "SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
100 [],
101 |r| r.get(0),
102 )?;
103 Ok(u32::try_from(v).unwrap_or(0))
104 }
105
106 pub fn node_count(&self) -> Result<u64, StoreError> {
111 let n: i64 = self
112 .conn
113 .query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))?;
114 Ok(u64::try_from(n).unwrap_or(0))
115 }
116
117 pub fn edge_count(&self) -> Result<u64, StoreError> {
122 let n: i64 = self
123 .conn
124 .query_row("SELECT COUNT(*) FROM edges", [], |r| r.get(0))?;
125 Ok(u64::try_from(n).unwrap_or(0))
126 }
127
128 pub fn upsert_node(&self, node: &Node) -> Result<(), StoreError> {
134 upsert_node(&self.conn, node)
135 }
136
137 pub fn insert_edge(&self, edge: &Edge) -> Result<(), StoreError> {
144 insert_edge(&self.conn, edge)
145 }
146
147 pub fn apply_factset(&mut self, facts: &FactSet) -> Result<(), StoreError> {
154 let tx = self.conn.transaction()?;
155 for node in &facts.nodes {
156 upsert_node(&tx, node)?;
157 }
158 for edge in &facts.edges {
159 insert_edge(&tx, edge)?;
160 }
161 tx.commit()?;
162 Ok(())
163 }
164
165 pub fn sync_state(&self) -> Result<Option<String>, StoreError> {
171 Ok(self
172 .conn
173 .query_row("SELECT tree FROM sync_state WHERE id = 0", [], |r| r.get(0))
174 .optional()?)
175 }
176
177 pub fn sync_env(&self) -> Result<Option<String>, StoreError> {
185 Ok(self
186 .conn
187 .query_row("SELECT env FROM sync_state WHERE id = 0", [], |r| r.get(0))
188 .optional()?
189 .flatten())
190 }
191
192 pub fn set_sync_env(&self, env: &str) -> Result<(), StoreError> {
199 self.conn
200 .execute("UPDATE sync_state SET env = ?1 WHERE id = 0", [env])?;
201 Ok(())
202 }
203
204 pub fn rebuild(&mut self, facts: &FactSet, tree: Option<&str>) -> Result<(), StoreError> {
217 let tx = self.conn.transaction()?;
218 tx.execute("DELETE FROM edges", [])?;
219 tx.execute("DELETE FROM nodes", [])?;
220 for node in &facts.nodes {
221 upsert_node(&tx, node)?;
222 }
223 for edge in &facts.edges {
224 insert_edge(&tx, edge)?;
225 }
226 write_sync_state(&tx, tree)?;
227 tx.commit()?;
228 Ok(())
229 }
230
231 pub fn reconcile(&mut self, facts: &FactSet, tree: Option<&str>) -> Result<(), StoreError> {
247 let current_nodes = self.all_nodes()?;
248 let current_edges = self.all_edges()?;
249 let cur_by_key: std::collections::HashMap<&str, &Node> =
250 current_nodes.iter().map(|n| (n.key.as_str(), n)).collect();
251 let new_keys: std::collections::HashSet<&str> =
252 facts.nodes.iter().map(|n| n.key.as_str()).collect();
253
254 let new_edge_ids: std::collections::HashSet<EdgeId> =
258 facts.edges.iter().map(edge_identity).collect();
259 let cur_edge_ids: std::collections::HashSet<EdgeId> =
260 current_edges.iter().map(edge_identity).collect();
261
262 let tx = self.conn.transaction()?;
263 for edge in ¤t_edges {
267 if !new_edge_ids.contains(&edge_identity(edge)) {
268 delete_edge(&tx, edge)?;
269 }
270 }
271 for old in ¤t_nodes {
273 if !new_keys.contains(old.key.as_str()) {
274 tx.execute("DELETE FROM nodes WHERE key = ?1", [&old.key])?;
275 }
276 }
277 for node in &facts.nodes {
280 if cur_by_key
281 .get(node.key.as_str())
282 .is_none_or(|cur| *cur != node)
283 {
284 upsert_node(&tx, node)?;
285 }
286 }
287 for edge in &facts.edges {
289 if !cur_edge_ids.contains(&edge_identity(edge)) {
290 insert_edge(&tx, edge)?;
291 }
292 }
293 write_sync_state(&tx, tree)?;
294 tx.commit()?;
295 Ok(())
296 }
297
298 pub fn get_node(&self, key: &str) -> Result<Option<Node>, StoreError> {
304 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.key = ?1");
305 let mut stmt = self.conn.prepare(&sql)?;
306 let mut rows = stmt.query([key])?;
307 match rows.next()? {
308 Some(row) => Ok(Some(row_to_node(row)?)),
309 None => Ok(None),
310 }
311 }
312
313 pub fn all_keys(&self) -> Result<Vec<String>, StoreError> {
318 let mut stmt = self.conn.prepare("SELECT key FROM nodes ORDER BY key")?;
319 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
320 let mut out = Vec::new();
321 for row in rows {
322 out.push(row?);
323 }
324 Ok(out)
325 }
326
327 pub fn export_factset(&self) -> Result<FactSet, StoreError> {
334 let node_sql = format!("SELECT {NODE_COLS} FROM nodes n ORDER BY n.key");
335 let mut node_stmt = self.conn.prepare(&node_sql)?;
336 let mut node_rows = node_stmt.query([])?;
337 let nodes = collect_nodes(&mut node_rows)?;
338
339 let edge_sql = format!("{EDGE_SELECT} ORDER BY ns.key, nd.key, e.kind, e.provenance");
342 let mut edge_stmt = self.conn.prepare(&edge_sql)?;
343 let mut edge_rows = edge_stmt.query([])?;
344 let edges = collect_edges(&mut edge_rows)?;
345
346 Ok(FactSet { nodes, edges })
347 }
348
349 pub fn nodes_by_kind(&self, kind: &NodeKind) -> Result<Vec<Node>, StoreError> {
355 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.kind = ?1 ORDER BY n.key");
356 let mut stmt = self.conn.prepare(&sql)?;
357 let mut rows = stmt.query([kind.as_str()])?;
358 collect_nodes(&mut rows)
359 }
360
361 pub fn nodes_by_kind_named(
371 &self,
372 kind: &NodeKind,
373 name_lower: &str,
374 ) -> Result<Vec<Node>, StoreError> {
375 let sql = format!(
376 "SELECT {NODE_COLS} FROM nodes n \
377 WHERE n.kind = ?1 AND lower(n.name) = ?2 ORDER BY n.key"
378 );
379 let mut stmt = self.conn.prepare(&sql)?;
380 let mut rows = stmt.query([kind.as_str(), name_lower])?;
381 collect_nodes(&mut rows)
382 }
383
384 pub fn config_keys(&self) -> Result<Vec<crate::ConfigKey>, StoreError> {
394 let nodes = self.nodes_by_kind(&NodeKind::Other(crate::config_keys::KIND.to_owned()))?;
395 let mut out = Vec::with_capacity(nodes.len());
396 for n in &nodes {
397 let key = n.meta.get("key").and_then(serde_json::Value::as_str);
398 let value = n.meta.get("value").and_then(serde_json::Value::as_str);
404 if let (Some(key), Some(file)) = (key, n.path.as_deref()) {
405 out.push(crate::ConfigKey {
406 file: file.to_owned(),
407 key: key.to_owned(),
408 value: value.unwrap_or_default().to_owned(),
409 value_known: value.is_some(),
410 });
411 }
412 }
413 Ok(out)
414 }
415
416 pub fn nodes_by_path(&self, path: &str) -> Result<Vec<Node>, StoreError> {
424 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.path = ?1 ORDER BY n.key");
425 let mut stmt = self.conn.prepare(&sql)?;
426 let mut rows = stmt.query([path])?;
427 collect_nodes(&mut rows)
428 }
429
430 pub fn nodes_by_provenance(&self, provenance: Provenance) -> Result<Vec<Node>, StoreError> {
438 let sql = format!("SELECT {NODE_COLS} FROM nodes n WHERE n.provenance = ?1 ORDER BY n.key");
439 let mut stmt = self.conn.prepare(&sql)?;
440 let mut rows = stmt.query([provenance.as_str()])?;
441 collect_nodes(&mut rows)
442 }
443
444 pub fn all_nodes(&self) -> Result<Vec<Node>, StoreError> {
451 let sql = format!("SELECT {NODE_COLS} FROM nodes n ORDER BY n.key");
452 let mut stmt = self.conn.prepare(&sql)?;
453 let mut rows = stmt.query([])?;
454 collect_nodes(&mut rows)
455 }
456
457 pub fn all_edges(&self) -> Result<Vec<Edge>, StoreError> {
469 let sql = format!("{EDGE_SELECT} ORDER BY ns.key, nd.key, e.kind, e.provenance");
470 let mut stmt = self.conn.prepare(&sql)?;
471 let mut rows = stmt.query([])?;
472 collect_edges(&mut rows)
473 }
474
475 pub fn edges_from(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
482 let sql = format!("{EDGE_SELECT} WHERE ns.key = ?1 ORDER BY nd.key, e.kind, e.provenance");
483 let mut stmt = self.conn.prepare(&sql)?;
484 let mut rows = stmt.query([key])?;
485 collect_edges(&mut rows)
486 }
487
488 pub fn edges_to(&self, key: &str) -> Result<Vec<Edge>, StoreError> {
494 let sql = format!("{EDGE_SELECT} WHERE nd.key = ?1 ORDER BY ns.key, e.kind, e.provenance");
495 let mut stmt = self.conn.prepare(&sql)?;
496 let mut rows = stmt.query([key])?;
497 collect_edges(&mut rows)
498 }
499
500 pub fn edges_by_provenance(&self, provenance: Provenance) -> Result<Vec<Edge>, StoreError> {
506 let sql = format!("{EDGE_SELECT} WHERE e.provenance = ?1 ORDER BY ns.key, nd.key, e.kind");
507 let mut stmt = self.conn.prepare(&sql)?;
508 let mut rows = stmt.query([provenance.as_str()])?;
509 collect_edges(&mut rows)
510 }
511
512 pub fn delete_edges_by_provenance(&self, provenance: Provenance) -> Result<u64, StoreError> {
519 let n = self.conn.execute(
520 "DELETE FROM edges WHERE provenance = ?1",
521 [provenance.as_str()],
522 )?;
523 Ok(u64::try_from(n).unwrap_or(0))
524 }
525
526 pub fn delete_edges_by_src_ref(&self, src_ref: &str) -> Result<u64, StoreError> {
534 let n = self
535 .conn
536 .execute("DELETE FROM edges WHERE src_ref = ?1", [src_ref])?;
537 Ok(u64::try_from(n).unwrap_or(0))
538 }
539
540 pub fn apply_import_layer(
555 &mut self,
556 src_ref: &str,
557 facts: &FactSet,
558 ) -> Result<ImportApplied, StoreError> {
559 let tx = self.conn.transaction()?;
560 tx.execute("DELETE FROM edges WHERE src_ref = ?1", [src_ref])?;
562 for node in &facts.nodes {
563 upsert_node(&tx, node)?;
564 }
565 let (kept, applied) = apply_edges_pruning(&tx, &facts.edges)?;
566 let trimmed = FactSet {
567 nodes: facts.nodes.clone(),
568 edges: kept,
569 };
570 put_import_row(&tx, src_ref, &trimmed)?;
571 tx.commit()?;
572 Ok(ImportApplied {
573 layers: 1,
574 nodes: facts.nodes.len(),
575 ..applied
576 })
577 }
578
579 pub fn delete_import(&self, src_ref: &str) -> Result<bool, StoreError> {
586 let n = self
587 .conn
588 .execute("DELETE FROM imports WHERE src_ref = ?1", [src_ref])?;
589 Ok(n > 0)
590 }
591
592 pub fn import_refs(&self) -> Result<Vec<String>, StoreError> {
597 let mut stmt = self
598 .conn
599 .prepare("SELECT src_ref FROM imports ORDER BY src_ref")?;
600 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
601 let mut out = Vec::new();
602 for row in rows {
603 out.push(row?);
604 }
605 Ok(out)
606 }
607
608 pub fn reapply_imports(&mut self) -> Result<ImportApplied, StoreError> {
619 let layers = self.load_import_layers()?;
620 let tx = self.conn.transaction()?;
621 for (_, facts) in &layers {
624 for node in &facts.nodes {
625 upsert_node(&tx, node)?;
626 }
627 }
628 let mut applied = ImportApplied {
630 layers: layers.len(),
631 ..ImportApplied::default()
632 };
633 for (src_ref, facts) in &layers {
634 applied.nodes += facts.nodes.len();
635 let (kept, counts) = apply_edges_pruning(&tx, &facts.edges)?;
636 applied.edges_applied += counts.edges_applied;
637 applied.edges_pruned += counts.edges_pruned;
638 if kept.len() != facts.edges.len() {
639 let trimmed = FactSet {
640 nodes: facts.nodes.clone(),
641 edges: kept,
642 };
643 put_import_row(&tx, src_ref, &trimmed)?;
644 }
645 }
646 tx.commit()?;
647 Ok(applied)
648 }
649
650 fn load_import_layers(&self) -> Result<Vec<(String, FactSet)>, StoreError> {
653 let mut stmt = self
654 .conn
655 .prepare("SELECT src_ref, facts FROM imports ORDER BY src_ref")?;
656 let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?;
657 let mut out = Vec::new();
658 for row in rows {
659 let (src_ref, json) = row?;
660 let mut facts: FactSet = serde_json::from_str(&json)?;
661 for node in &mut facts.nodes {
673 if node.provenance == Provenance::Derived {
674 node.provenance = import_node_provenance(&node.key);
675 }
676 }
677 out.push((src_ref, facts));
678 }
679 Ok(out)
680 }
681
682 pub fn neighbors(&self, key: &str, dir: Direction) -> Result<Vec<Node>, StoreError> {
689 let out = format!(
690 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.dst \
691 JOIN nodes s ON s.id = e.src WHERE s.key = ?1"
692 );
693 let inc = format!(
694 "SELECT {NODE_COLS} FROM nodes n JOIN edges e ON n.id = e.src \
695 JOIN nodes d ON d.id = e.dst WHERE d.key = ?1"
696 );
697 let sql = match dir {
702 Direction::Outgoing => format!("{out} ORDER BY 1"),
703 Direction::Incoming => format!("{inc} ORDER BY 1"),
704 Direction::Both => format!("{out} UNION {inc} ORDER BY 1"),
705 };
706 let mut stmt = self.conn.prepare(&sql)?;
707 let mut rows = stmt.query([key])?;
708 collect_nodes(&mut rows)
709 }
710
711 pub fn context_cache_get(&self, key: &str) -> Result<Option<(String, String)>, StoreError> {
718 let row = self
719 .conn
720 .query_row(
721 "SELECT fingerprint, json FROM node_context WHERE key = ?1",
722 [key],
723 |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
724 )
725 .optional()?;
726 Ok(row)
727 }
728
729 pub fn context_cache_fingerprint(&self, key: &str) -> Result<Option<String>, StoreError> {
735 let fp = self
736 .conn
737 .query_row(
738 "SELECT fingerprint FROM node_context WHERE key = ?1",
739 [key],
740 |r| r.get::<_, String>(0),
741 )
742 .optional()?;
743 Ok(fp)
744 }
745
746 pub fn context_cache_put(
751 &self,
752 key: &str,
753 fingerprint: &str,
754 json: &str,
755 ) -> Result<(), StoreError> {
756 self.conn.execute(
757 "INSERT INTO node_context (key, fingerprint, json) VALUES (?1, ?2, ?3)
758 ON CONFLICT(key) DO UPDATE SET
759 fingerprint = excluded.fingerprint, json = excluded.json",
760 [key, fingerprint, json],
761 )?;
762 Ok(())
763 }
764
765 pub fn context_cache_delete(&self, key: &str) -> Result<bool, StoreError> {
770 let n = self
771 .conn
772 .execute("DELETE FROM node_context WHERE key = ?1", [key])?;
773 Ok(n > 0)
774 }
775
776 pub fn context_cache_keys(&self) -> Result<Vec<String>, StoreError> {
782 let mut stmt = self
783 .conn
784 .prepare("SELECT key FROM node_context ORDER BY key")?;
785 let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
786 let mut out = Vec::new();
787 for row in rows {
788 out.push(row?);
789 }
790 Ok(out)
791 }
792}
793
794fn node_row_id(conn: &Connection, key: &str) -> rusqlite::Result<Option<i64>> {
797 conn.query_row("SELECT id FROM nodes WHERE key = ?1", [key], |r| r.get(0))
798 .optional()
799}
800
801fn write_sync_state(conn: &Connection, tree: Option<&str>) -> Result<(), StoreError> {
804 match tree {
805 Some(tree) => conn.execute(
811 "INSERT INTO sync_state (id, tree) VALUES (0, ?1)
812 ON CONFLICT(id) DO UPDATE SET tree = excluded.tree, env = NULL",
813 [tree],
814 )?,
815 None => conn.execute("DELETE FROM sync_state WHERE id = 0", [])?,
816 };
817 Ok(())
818}
819
820fn upsert_node(conn: &Connection, node: &Node) -> Result<(), StoreError> {
821 let meta = serde_json::to_string(&node.meta)?;
822 let (span_start, span_end) = match node.span {
823 Some(s) => (Some(i64::from(s.start)), Some(i64::from(s.end))),
824 None => (None, None),
825 };
826 conn.execute(
827 "INSERT INTO nodes (key, kind, name, path, lang, blob_hash, span_start, span_end, provenance, meta)
828 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
829 ON CONFLICT(key) DO UPDATE SET
830 kind = excluded.kind, name = excluded.name, path = excluded.path,
831 lang = excluded.lang, blob_hash = excluded.blob_hash,
832 span_start = excluded.span_start, span_end = excluded.span_end,
833 provenance = excluded.provenance, meta = excluded.meta",
834 params![
835 node.key,
836 node.kind.as_str(),
837 node.name,
838 node.path,
839 node.lang,
840 node.blob_hash,
841 span_start,
842 span_end,
843 node.provenance.as_str(),
844 meta,
845 ],
846 )?;
847 Ok(())
848}
849
850fn insert_edge(conn: &Connection, edge: &Edge) -> Result<(), StoreError> {
851 validate_edge(edge)?;
852 let src_id =
853 node_row_id(conn, &edge.src)?.ok_or_else(|| StoreError::UnknownNode(edge.src.clone()))?;
854 let dst_id =
855 node_row_id(conn, &edge.dst)?.ok_or_else(|| StoreError::UnknownNode(edge.dst.clone()))?;
856 insert_edge_row(conn, edge, src_id, dst_id)
857}
858
859fn insert_edge_if_present(conn: &Connection, edge: &Edge) -> Result<bool, StoreError> {
865 validate_edge(edge)?;
866 let (Some(src_id), Some(dst_id)) =
867 (node_row_id(conn, &edge.src)?, node_row_id(conn, &edge.dst)?)
868 else {
869 return Ok(false);
870 };
871 insert_edge_row(conn, edge, src_id, dst_id)?;
872 Ok(true)
873}
874
875fn apply_edges_pruning(
879 conn: &Connection,
880 edges: &[Edge],
881) -> Result<(Vec<Edge>, ImportApplied), StoreError> {
882 let mut kept = Vec::with_capacity(edges.len());
883 let mut counts = ImportApplied::default();
884 for edge in edges {
885 if insert_edge_if_present(conn, edge)? {
886 kept.push(edge.clone());
887 counts.edges_applied += 1;
888 } else {
889 counts.edges_pruned += 1;
890 }
891 }
892 Ok((kept, counts))
893}
894
895fn put_import_row(conn: &Connection, src_ref: &str, facts: &FactSet) -> Result<(), StoreError> {
898 let json = serde_json::to_string(facts)?;
899 conn.execute(
900 "INSERT INTO imports (src_ref, facts) VALUES (?1, ?2)
901 ON CONFLICT(src_ref) DO UPDATE SET facts = excluded.facts, imported_at = datetime('now')",
902 params![src_ref, json],
903 )?;
904 Ok(())
905}
906
907fn validate_edge(edge: &Edge) -> Result<(), StoreError> {
910 if edge.is_valid() {
911 Ok(())
912 } else {
913 Err(StoreError::InvalidEdge(format!(
914 "confidence must be present iff provenance is inferred (src={}, dst={})",
915 edge.src, edge.dst
916 )))
917 }
918}
919
920fn insert_edge_row(
925 conn: &Connection,
926 edge: &Edge,
927 src_id: i64,
928 dst_id: i64,
929) -> Result<(), StoreError> {
930 conn.execute(
931 "INSERT INTO edges (src, dst, kind, provenance, confidence, src_ref)
932 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
933 ON CONFLICT(src, dst, kind, provenance) DO NOTHING",
934 params![
935 src_id,
936 dst_id,
937 edge.kind.as_str(),
938 edge.provenance.as_str(),
939 edge.confidence,
940 edge.src_ref,
941 ],
942 )?;
943 Ok(())
944}
945
946fn edge_identity(edge: &Edge) -> EdgeId {
954 (
955 edge.src.clone(),
956 edge.dst.clone(),
957 edge.kind.as_str().to_owned(),
958 edge.provenance.as_str().to_owned(),
959 edge.confidence.map(f64::to_bits),
960 edge.src_ref.clone(),
961 )
962}
963
964type EdgeId = (String, String, String, String, Option<u64>, Option<String>);
967
968fn delete_edge(conn: &Connection, edge: &Edge) -> Result<(), StoreError> {
971 conn.execute(
972 "DELETE FROM edges
973 WHERE src = (SELECT id FROM nodes WHERE key = ?1)
974 AND dst = (SELECT id FROM nodes WHERE key = ?2)
975 AND kind = ?3 AND provenance = ?4",
976 params![
977 edge.src,
978 edge.dst,
979 edge.kind.as_str(),
980 edge.provenance.as_str()
981 ],
982 )?;
983 Ok(())
984}
985
986fn collect_nodes(rows: &mut rusqlite::Rows) -> Result<Vec<Node>, StoreError> {
987 let mut out = Vec::new();
988 while let Some(row) = rows.next()? {
989 out.push(row_to_node(row)?);
990 }
991 Ok(out)
992}
993
994fn collect_edges(rows: &mut rusqlite::Rows) -> Result<Vec<Edge>, StoreError> {
995 let mut out = Vec::new();
996 while let Some(row) = rows.next()? {
997 out.push(row_to_edge(row)?);
998 }
999 Ok(out)
1000}
1001
1002fn row_to_node(row: &rusqlite::Row) -> Result<Node, StoreError> {
1003 let kind: String = row.get("kind")?;
1004 let span_start: Option<i64> = row.get("span_start")?;
1005 let span_end: Option<i64> = row.get("span_end")?;
1006 let span = match (span_start, span_end) {
1007 (Some(s), Some(e)) => Some(Span::new(to_u32(s)?, to_u32(e)?)),
1008 _ => None,
1009 };
1010 let meta: String = row.get("meta")?;
1011 let provenance: String = row.get("provenance")?;
1012 let provenance = Provenance::from_token(&provenance)
1013 .ok_or_else(|| StoreError::Corrupt(format!("unknown node provenance: {provenance}")))?;
1014 Ok(Node {
1015 key: row.get("key")?,
1016 kind: NodeKind::from_token(&kind),
1017 name: row.get("name")?,
1018 path: row.get("path")?,
1019 lang: row.get("lang")?,
1020 blob_hash: row.get("blob_hash")?,
1021 span,
1022 provenance,
1023 meta: serde_json::from_str(&meta)?,
1024 })
1025}
1026
1027fn row_to_edge(row: &rusqlite::Row) -> Result<Edge, StoreError> {
1028 let kind: String = row.get("kind")?;
1029 let provenance: String = row.get("provenance")?;
1030 let provenance = Provenance::from_token(&provenance)
1031 .ok_or_else(|| StoreError::Corrupt(format!("unknown provenance: {provenance}")))?;
1032 Ok(Edge {
1033 src: row.get("src")?,
1034 dst: row.get("dst")?,
1035 kind: EdgeKind::from_token(&kind),
1036 provenance,
1037 confidence: row.get("confidence")?,
1038 src_ref: row.get("src_ref")?,
1039 })
1040}
1041
1042fn to_u32(v: i64) -> Result<u32, StoreError> {
1043 u32::try_from(v).map_err(|_| StoreError::Corrupt(format!("span offset out of range: {v}")))
1044}
1045
1046fn import_node_provenance(key: &str) -> Provenance {
1051 if key.starts_with("graphify:") {
1052 Provenance::Inferred
1053 } else {
1054 Provenance::Authored
1055 }
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060 use super::Store;
1061 use crate::model::{Direction, Edge, EdgeKind, FactSet, Node, NodeKind, Span};
1062 use crate::provenance::Provenance;
1063
1064 fn sample_node(key: &str) -> Node {
1065 Node {
1066 key: key.to_owned(),
1067 kind: NodeKind::Fn,
1068 name: "sample".to_owned(),
1069 path: Some("src/lib.rs".to_owned()),
1070 lang: Some("rust".to_owned()),
1071 blob_hash: Some("deadbeef".to_owned()),
1072 span: Some(Span::new(10, 42)),
1073 provenance: Provenance::Derived,
1074 meta: serde_json::json!({"vis": "pub"}),
1075 }
1076 }
1077
1078 #[test]
1079 fn reconcile_matches_a_full_rebuild() {
1080 let node = |k: &str, name: &str| {
1083 let mut n = sample_node(k);
1084 n.name = name.to_owned();
1085 n
1086 };
1087 let edge =
1088 |src: &str, dst: &str| Edge::derived(src.to_owned(), dst.to_owned(), EdgeKind::Calls);
1089 let inferred = |src: &str, dst: &str, conf: f64| {
1092 let mut e = Edge::inferred(src.to_owned(), dst.to_owned(), EdgeKind::Related, conf);
1093 e.src_ref = Some("import:demo".to_owned());
1094 e
1095 };
1096
1097 let facts1 = FactSet {
1098 nodes: vec![node("a", "A"), node("b", "B"), node("c", "C")],
1099 edges: vec![edge("a", "b"), edge("b", "c"), inferred("a", "c", 0.7)],
1100 };
1101 let facts2 = FactSet {
1104 nodes: vec![node("a", "A"), node("b", "B2"), node("d", "D")],
1105 edges: vec![edge("a", "b"), edge("a", "d"), inferred("a", "d", 0.9)],
1106 };
1107
1108 let mut reconciled = Store::open_in_memory().expect("open");
1110 reconciled.rebuild(&facts1, Some("t1")).expect("rebuild");
1111 reconciled
1112 .reconcile(&facts2, Some("t2"))
1113 .expect("reconcile");
1114
1115 let mut rebuilt = Store::open_in_memory().expect("open");
1117 rebuilt.rebuild(&facts2, Some("t2")).expect("rebuild");
1118
1119 let canon = |fs: FactSet| {
1120 let mut nodes = fs.nodes;
1121 nodes.sort_by(|a, b| a.key.cmp(&b.key));
1122 let mut edges: Vec<String> = fs
1123 .edges
1124 .iter()
1125 .map(|e| {
1126 format!(
1127 "{}\0{}\0{}\0{}\0{:?}\0{:?}",
1128 e.kind.as_str(),
1129 e.src,
1130 e.dst,
1131 e.provenance.as_str(),
1132 e.confidence,
1133 e.src_ref
1134 )
1135 })
1136 .collect();
1137 edges.sort();
1138 (nodes, edges)
1139 };
1140 assert_eq!(
1141 canon(reconciled.export_factset().expect("export")),
1142 canon(rebuilt.export_factset().expect("export")),
1143 "reconcile must match a full rebuild",
1144 );
1145 assert_eq!(
1146 reconciled.sync_state().expect("state").as_deref(),
1147 Some("t2")
1148 );
1149 }
1150
1151 #[test]
1152 fn reconcile_writes_only_the_edge_delta() {
1153 let n = |k: &str| sample_node(k);
1157 let e =
1158 |src: &str, dst: &str| Edge::derived(src.to_owned(), dst.to_owned(), EdgeKind::Calls);
1159
1160 let mut store = Store::open_in_memory().expect("open");
1161 store
1162 .rebuild(
1163 &FactSet {
1164 nodes: vec![n("a"), n("b"), n("c")],
1165 edges: vec![e("a", "b"), e("b", "c")],
1166 },
1167 None,
1168 )
1169 .expect("rebuild");
1170
1171 let rowids = |store: &Store| -> std::collections::HashMap<(String, String), i64> {
1173 let mut stmt = store
1174 .conn
1175 .prepare(
1176 "SELECT ns.key, nd.key, e.rowid FROM edges e \
1177 JOIN nodes ns ON ns.id = e.src JOIN nodes nd ON nd.id = e.dst",
1178 )
1179 .expect("prepare");
1180 stmt.query_map([], |r| {
1181 Ok((
1182 (r.get::<_, String>(0)?, r.get::<_, String>(1)?),
1183 r.get::<_, i64>(2)?,
1184 ))
1185 })
1186 .expect("query")
1187 .map(Result::unwrap)
1188 .collect()
1189 };
1190
1191 let before = rowids(&store);
1192 let ab_rowid = before[&("a".to_owned(), "b".to_owned())];
1193
1194 store
1196 .reconcile(
1197 &FactSet {
1198 nodes: vec![n("a"), n("b"), n("c")],
1199 edges: vec![e("a", "b"), e("a", "c")],
1200 },
1201 None,
1202 )
1203 .expect("reconcile");
1204
1205 let after = rowids(&store);
1206 assert_eq!(
1207 after.get(&("a".to_owned(), "b".to_owned())),
1208 Some(&ab_rowid),
1209 "the unchanged edge keeps its row (not rewritten)"
1210 );
1211 assert!(
1212 !after.contains_key(&("b".to_owned(), "c".to_owned())),
1213 "the removed edge's row is gone"
1214 );
1215 assert!(
1216 after.contains_key(&("a".to_owned(), "c".to_owned())),
1217 "the added edge has a new row"
1218 );
1219 }
1220
1221 #[test]
1222 fn reconcile_is_history_independent_for_edge_queries() {
1223 let n = |k: &str| sample_node(k);
1228 let d =
1229 |src: &str, dst: &str| Edge::derived(src.to_owned(), dst.to_owned(), EdgeKind::Calls);
1230 let inf = |src: &str, dst: &str, c: f64| {
1231 Edge::inferred(src.to_owned(), dst.to_owned(), EdgeKind::Related, c)
1232 };
1233 let f1 = FactSet {
1234 nodes: vec![n("a"), n("b"), n("c")],
1235 edges: vec![d("a", "b"), d("b", "c"), inf("a", "c", 0.7)],
1236 };
1237 let f2 = FactSet {
1238 nodes: vec![n("a"), n("b"), n("d")],
1239 edges: vec![d("a", "b"), d("a", "d"), inf("a", "d", 0.9)],
1240 };
1241
1242 let mut incremental = Store::open_in_memory().expect("open");
1243 incremental.rebuild(&f1, None).expect("rebuild");
1244 incremental.reconcile(&f2, None).expect("reconcile");
1245 let mut cold = Store::open_in_memory().expect("open");
1246 cold.rebuild(&f2, None).expect("rebuild");
1247
1248 let proj = |es: Vec<Edge>| -> Vec<String> {
1250 es.into_iter()
1251 .map(|e| {
1252 format!(
1253 "{}|{}|{}|{}|{:?}|{:?}",
1254 e.src,
1255 e.dst,
1256 e.kind.as_str(),
1257 e.provenance.as_str(),
1258 e.confidence,
1259 e.src_ref
1260 )
1261 })
1262 .collect()
1263 };
1264
1265 for key in ["a", "b", "d"] {
1266 assert_eq!(
1267 proj(incremental.edges_from(key).expect("from")),
1268 proj(cold.edges_from(key).expect("from")),
1269 "edges_from({key}) must match a cold rebuild"
1270 );
1271 assert_eq!(
1272 proj(incremental.edges_to(key).expect("to")),
1273 proj(cold.edges_to(key).expect("to")),
1274 "edges_to({key}) must match a cold rebuild"
1275 );
1276 }
1277 assert_eq!(
1278 proj(incremental.all_edges().expect("all")),
1279 proj(cold.all_edges().expect("all")),
1280 "all_edges must match a cold rebuild"
1281 );
1282 for p in [Provenance::Derived, Provenance::Inferred] {
1283 assert_eq!(
1284 proj(incremental.edges_by_provenance(p).expect("prov")),
1285 proj(cold.edges_by_provenance(p).expect("prov")),
1286 "edges_by_provenance({}) must match a cold rebuild",
1287 p.as_str()
1288 );
1289 }
1290 }
1291
1292 #[test]
1293 fn reconcile_updates_confidence_on_an_unchanged_tuple() {
1294 let n = |k: &str| sample_node(k);
1299 let inf = |c: f64| {
1300 let mut e = Edge::inferred("a".to_owned(), "b".to_owned(), EdgeKind::Related, c);
1301 e.src_ref = Some("import:demo".to_owned());
1302 e
1303 };
1304
1305 let mut store = Store::open_in_memory().expect("open");
1306 store
1307 .rebuild(
1308 &FactSet {
1309 nodes: vec![n("a"), n("b")],
1310 edges: vec![inf(0.5)],
1311 },
1312 None,
1313 )
1314 .expect("rebuild");
1315 store
1316 .reconcile(
1317 &FactSet {
1318 nodes: vec![n("a"), n("b")],
1319 edges: vec![inf(0.9)],
1320 },
1321 None,
1322 )
1323 .expect("reconcile");
1324
1325 let edges = store.edges_from("a").expect("edges");
1326 assert_eq!(edges.len(), 1);
1327 assert_eq!(
1328 edges[0].confidence,
1329 Some(0.9),
1330 "confidence updated, not left stale"
1331 );
1332 }
1333
1334 #[test]
1335 fn edge_identity_does_not_collide_across_field_boundaries() {
1336 let e1 = super::edge_identity(&Edge::derived(
1341 "a\u{1f}b".to_owned(),
1342 "c".to_owned(),
1343 EdgeKind::Calls,
1344 ));
1345 let e2 = super::edge_identity(&Edge::derived(
1346 "a".to_owned(),
1347 "b\u{1f}c".to_owned(),
1348 EdgeKind::Calls,
1349 ));
1350 assert_ne!(
1351 e1, e2,
1352 "control chars in a key must not collapse identities"
1353 );
1354
1355 let derived = super::edge_identity(&Edge::derived(
1358 "a".to_owned(),
1359 "b".to_owned(),
1360 EdgeKind::Related,
1361 ));
1362 let inferred0 = super::edge_identity(&Edge::inferred(
1363 "a".to_owned(),
1364 "b".to_owned(),
1365 EdgeKind::Related,
1366 0.0,
1367 ));
1368 assert_ne!(derived, inferred0, "None vs Some(0.0) confidence differ");
1369 }
1370
1371 #[test]
1372 fn open_in_memory_applies_schema() {
1373 let store = Store::open_in_memory().expect("open");
1374 assert_eq!(store.node_count().expect("count"), 0);
1375 assert_eq!(store.schema_version().expect("version"), 7);
1376 }
1377
1378 #[test]
1379 fn upsert_and_get_round_trips_all_fields() {
1380 let store = Store::open_in_memory().expect("open");
1381 let node = sample_node("sym:rust:src/lib.rs#sample");
1382 store.upsert_node(&node).expect("upsert");
1383 let got = store.get_node(&node.key).expect("get").expect("present");
1384 assert_eq!(got, node);
1385 }
1386
1387 #[test]
1388 fn upsert_updates_in_place() {
1389 let store = Store::open_in_memory().expect("open");
1390 let mut node = sample_node("k");
1391 store.upsert_node(&node).expect("insert");
1392 node.name = "renamed".to_owned();
1393 node.kind = NodeKind::Struct;
1394 store.upsert_node(&node).expect("update");
1395 assert_eq!(store.node_count().expect("count"), 1);
1396 let got = store.get_node("k").expect("get").expect("present");
1397 assert_eq!(got.name, "renamed");
1398 assert_eq!(got.kind, NodeKind::Struct);
1399 }
1400
1401 #[test]
1402 fn edge_with_unknown_endpoint_is_rejected() {
1403 let store = Store::open_in_memory().expect("open");
1404 store
1405 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
1406 .expect("a");
1407 let edge = Edge::derived("a", "missing", EdgeKind::Calls);
1408 let err = store.insert_edge(&edge).expect_err("should reject");
1409 assert!(matches!(err, super::StoreError::UnknownNode(k) if k == "missing"));
1410 }
1411
1412 #[test]
1413 fn inferred_edge_requires_confidence() {
1414 let store = Store::open_in_memory().expect("open");
1415 store
1416 .upsert_node(&Node::new("a", NodeKind::Fn, "a"))
1417 .expect("a");
1418 store
1419 .upsert_node(&Node::new("b", NodeKind::Fn, "b"))
1420 .expect("b");
1421 let bad = Edge {
1423 src: "a".to_owned(),
1424 dst: "b".to_owned(),
1425 kind: EdgeKind::References,
1426 provenance: Provenance::Inferred,
1427 confidence: None,
1428 src_ref: None,
1429 };
1430 assert!(matches!(
1431 store.insert_edge(&bad).expect_err("reject"),
1432 super::StoreError::InvalidEdge(_)
1433 ));
1434 }
1435
1436 #[test]
1437 fn apply_factset_is_atomic() {
1438 let mut store = Store::open_in_memory().expect("open");
1439 let facts = FactSet::new()
1441 .with_node(Node::new("a", NodeKind::Fn, "a"))
1442 .with_node(Node::new("b", NodeKind::Fn, "b"))
1443 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
1444 .with_edge(Edge::derived("a", "ghost", EdgeKind::Calls));
1445 assert!(store.apply_factset(&facts).is_err());
1446 assert_eq!(store.node_count().expect("count"), 0, "rolled back");
1447 assert_eq!(store.edge_count().expect("count"), 0, "rolled back");
1448 }
1449
1450 #[test]
1451 fn neighbors_and_provenance_queries() {
1452 let mut store = Store::open_in_memory().expect("open");
1453 let facts = FactSet::new()
1454 .with_node(Node::new("a", NodeKind::Fn, "a"))
1455 .with_node(Node::new("b", NodeKind::Fn, "b"))
1456 .with_node(Node::new("c", NodeKind::Fn, "c"))
1457 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
1458 .with_edge(Edge::inferred("a", "c", EdgeKind::References, 0.5));
1459 store.apply_factset(&facts).expect("apply");
1460
1461 let out = store.neighbors("a", Direction::Outgoing).expect("out");
1462 let mut keys: Vec<_> = out.iter().map(|n| n.key.clone()).collect();
1463 keys.sort();
1464 assert_eq!(keys, ["b", "c"]);
1465
1466 assert!(
1467 store
1468 .neighbors("b", Direction::Outgoing)
1469 .expect("b out")
1470 .is_empty()
1471 );
1472 assert_eq!(
1473 store
1474 .neighbors("b", Direction::Incoming)
1475 .expect("b in")
1476 .len(),
1477 1
1478 );
1479
1480 let inferred = store
1481 .edges_by_provenance(Provenance::Inferred)
1482 .expect("inf");
1483 assert_eq!(inferred.len(), 1);
1484 assert_eq!(inferred[0].confidence, Some(0.5));
1485 }
1486
1487 #[test]
1488 fn neighbors_of_absent_node_is_empty() {
1489 let store = Store::open_in_memory().expect("open");
1490 assert!(
1491 store
1492 .neighbors("nope", Direction::Both)
1493 .expect("q")
1494 .is_empty()
1495 );
1496 }
1497
1498 #[test]
1499 fn get_missing_node_is_none() {
1500 let store = Store::open_in_memory().expect("open");
1501 assert!(store.get_node("absent").expect("get").is_none());
1502 }
1503
1504 #[test]
1505 fn nodes_by_kind_and_edges_to() {
1506 let mut store = Store::open_in_memory().expect("open");
1507 let facts = FactSet::new()
1508 .with_node(Node::new("f1", NodeKind::Fn, "f1"))
1509 .with_node(Node::new("f2", NodeKind::Fn, "f2"))
1510 .with_node(Node::new("s1", NodeKind::Struct, "s1"))
1511 .with_edge(Edge::derived("f1", "s1", EdgeKind::References))
1512 .with_edge(Edge::derived("f2", "s1", EdgeKind::References));
1513 store.apply_factset(&facts).expect("apply");
1514
1515 let fns = store.nodes_by_kind(&NodeKind::Fn).expect("fns");
1516 assert_eq!(
1517 fns.iter().map(|n| n.key.as_str()).collect::<Vec<_>>(),
1518 ["f1", "f2"]
1519 );
1520 assert!(
1521 store
1522 .nodes_by_kind(&NodeKind::Enum)
1523 .expect("enums")
1524 .is_empty()
1525 );
1526
1527 let into_s1 = store.edges_to("s1").expect("edges_to");
1528 assert_eq!(into_s1.len(), 2);
1529 assert!(into_s1.iter().all(|e| e.dst == "s1"));
1530 }
1531
1532 #[test]
1533 fn open_persists_across_reopen() {
1534 let path =
1535 std::env::temp_dir().join(format!("roteiro-open-test-{}.db", std::process::id()));
1536 std::fs::remove_file(&path).ok();
1537 {
1538 let store = Store::open(&path).expect("open");
1539 store
1540 .upsert_node(&sample_node("persisted"))
1541 .expect("upsert");
1542 }
1543 {
1544 let store = Store::open(&path).expect("reopen");
1545 assert_eq!(store.node_count().expect("count"), 1);
1546 assert_eq!(store.schema_version().expect("version"), 7);
1547 assert!(store.get_node("persisted").expect("get").is_some());
1548 }
1549 std::fs::remove_file(&path).expect("cleanup");
1550 }
1551
1552 fn graphify_layer(dst: &str) -> FactSet {
1553 FactSet::new()
1554 .with_node(Node::new("graphify:doc1", NodeKind::Doc, "Doc 1"))
1555 .with_edge({
1556 let mut e = Edge::inferred("graphify:doc1", dst, EdgeKind::References, 0.9);
1557 e.src_ref = Some("import:graphify".to_owned());
1558 e
1559 })
1560 }
1561
1562 #[test]
1565 fn imports_survive_rebuild() {
1566 let mut store = Store::open_in_memory().expect("open");
1567 let derived = FactSet::new().with_node(Node::new("file:a.rs", NodeKind::File, "a.rs"));
1568 store.rebuild(&derived, Some("tree1")).expect("rebuild");
1569
1570 let applied = store
1572 .apply_import_layer("import:graphify", &graphify_layer("file:a.rs"))
1573 .expect("apply import");
1574 assert_eq!(applied.edges_applied, 1);
1575 assert_eq!(applied.edges_pruned, 0);
1576 assert_eq!(store.import_refs().expect("refs"), vec!["import:graphify"]);
1577
1578 store.rebuild(&derived, Some("tree2")).expect("rebuild2");
1581 assert!(store.get_node("graphify:doc1").expect("get").is_none());
1582
1583 let applied = store.reapply_imports().expect("reapply");
1585 assert_eq!(applied.layers, 1);
1586 assert_eq!(applied.nodes, 1);
1587 assert_eq!(applied.edges_applied, 1);
1588 assert_eq!(applied.edges_pruned, 0);
1589 assert!(store.get_node("graphify:doc1").expect("get").is_some());
1590 assert_eq!(store.edges_from("graphify:doc1").expect("edges").len(), 1);
1591 }
1592
1593 #[test]
1597 fn reapply_prunes_stale_cross_references() {
1598 let mut store = Store::open_in_memory().expect("open");
1599 let derived = FactSet::new().with_node(Node::new("file:gone.rs", NodeKind::File, "g"));
1600 store.rebuild(&derived, Some("t1")).expect("rebuild");
1601 store
1602 .apply_import_layer("import:graphify", &graphify_layer("file:gone.rs"))
1603 .expect("import");
1604
1605 store
1607 .rebuild(&FactSet::new(), Some("t2"))
1608 .expect("rebuild2");
1609 let applied = store.reapply_imports().expect("reapply");
1610 assert_eq!(applied.nodes, 1);
1611 assert_eq!(applied.edges_applied, 0);
1612 assert_eq!(
1613 applied.edges_pruned, 1,
1614 "the edge to the deleted file is pruned"
1615 );
1616 assert!(store.get_node("graphify:doc1").expect("get").is_some());
1617
1618 let again = store.reapply_imports().expect("reapply2");
1621 assert_eq!(again.edges_applied, 0);
1622 assert_eq!(again.edges_pruned, 0, "already pruned; not retried");
1623 }
1624
1625 #[test]
1628 fn apply_import_layer_prunes_on_import() {
1629 let mut store = Store::open_in_memory().expect("open");
1630 let present = || FactSet::new().with_node(Node::new("file:a.rs", NodeKind::File, "a"));
1631 store.rebuild(&present(), Some("t")).expect("rebuild");
1632
1633 let layer = graphify_layer("file:a.rs").with_edge({
1634 let mut e = Edge::inferred("graphify:doc1", "file:ghost.rs", EdgeKind::References, 0.9);
1636 e.src_ref = Some("import:graphify".to_owned());
1637 e
1638 });
1639 let applied = store
1640 .apply_import_layer("import:graphify", &layer)
1641 .expect("import");
1642 assert_eq!(applied.edges_applied, 1);
1643 assert_eq!(applied.edges_pruned, 1);
1644
1645 store.rebuild(&present(), Some("t2")).expect("rebuild2");
1647 let re = store.reapply_imports().expect("reapply");
1648 assert_eq!(re.edges_applied, 1);
1649 assert_eq!(re.edges_pruned, 0, "ghost edge was not persisted");
1650 }
1651
1652 #[test]
1653 fn legacy_import_layer_nodes_are_retagged_non_derived() {
1654 let mut store = Store::open_in_memory().expect("open");
1659 let legacy = r#"{"nodes":[
1660 {"key":"graphify:doc1","kind":"doc","name":"d","path":null,"lang":null,"blob_hash":null,"span":null,"meta":null},
1661 {"key":"lat:lat.md/a.md","kind":"doc","name":"a","path":null,"lang":null,"blob_hash":null,"span":null,"meta":null}
1662 ],"edges":[]}"#;
1663 store
1664 .conn
1665 .execute(
1666 "INSERT INTO imports (src_ref, facts) VALUES ('import:legacy', ?1)",
1667 [legacy],
1668 )
1669 .expect("seed legacy import row");
1670
1671 store.reapply_imports().expect("reapply");
1672
1673 let g = store
1674 .get_node("graphify:doc1")
1675 .expect("get")
1676 .expect("graphify node");
1677 assert_eq!(
1678 g.provenance,
1679 Provenance::Inferred,
1680 "graphify import node repaired to inferred"
1681 );
1682 let l = store
1683 .get_node("lat:lat.md/a.md")
1684 .expect("get")
1685 .expect("lat node");
1686 assert_eq!(
1687 l.provenance,
1688 Provenance::Authored,
1689 "lat import node repaired to authored"
1690 );
1691 }
1692
1693 #[test]
1695 fn apply_import_replaces_and_delete_removes() {
1696 let mut store = Store::open_in_memory().expect("open");
1697 let a = FactSet::new().with_node(Node::new("graphify:x", NodeKind::Doc, "x"));
1698 let b = FactSet::new().with_node(Node::new("graphify:y", NodeKind::Doc, "y"));
1699 store.apply_import_layer("import:graphify", &a).expect("a");
1700 store.apply_import_layer("import:graphify", &b).expect("b");
1701 assert_eq!(
1702 store.import_refs().expect("refs").len(),
1703 1,
1704 "same ref replaced"
1705 );
1706 assert!(store.delete_import("import:graphify").expect("del"));
1707 assert!(store.import_refs().expect("refs").is_empty());
1708 assert!(!store.delete_import("import:graphify").expect("del again"));
1709 }
1710}