Skip to main content

lsp_max/runtime/control_plane/semantic_graph/
store.rs

1//! `OxigraphStore` — the ONLY place `oxigraph::*` appears outside tests.
2//!
3//! All other modules MUST interact with Oxigraph exclusively through the
4//! `SemanticLawGraph` trait defined in `mod.rs`.
5//!
6//! # INVARIANT: OXIGRAPH_BOUNDARY_HELD
7//! `OxigraphStore` is the single implementation of `SemanticLawGraph` that
8//! touches `oxigraph::*`.  No other module in `src/` may import `oxigraph`
9//! for semantic-graph purposes.
10
11use oxigraph::model::Term;
12use oxigraph::sparql::{QueryResults, SparqlEvaluator};
13use oxigraph::store::Store;
14
15use super::{
16    snapshot::{GraphDigest, LawGraphSnapshot},
17    SemanticLawGraph,
18};
19
20/// Oxigraph-backed implementation of `SemanticLawGraph`.
21///
22/// Uses an in-memory Oxigraph `Store` for SPARQL query evaluation.
23/// This struct is the sole owner of all oxigraph imports in production code.
24pub struct OxigraphStore {
25    store: Store,
26}
27
28impl OxigraphStore {
29    /// Create a new empty in-memory store.
30    pub fn new() -> Result<Self, String> {
31        Ok(Self {
32            store: Store::new().map_err(|e| e.to_string())?,
33        })
34    }
35}
36
37impl Default for OxigraphStore {
38    fn default() -> Self {
39        Self::new().expect("in-memory Oxigraph store must not fail")
40    }
41}
42
43impl SemanticLawGraph for OxigraphStore {
44    fn load_snapshot(&self, snapshot: LawGraphSnapshot) -> Result<GraphDigest, String> {
45        let digest = snapshot.digest();
46        self.store
47            .load_from_reader(oxigraph::io::RdfFormat::NQuads, snapshot.nquads.as_bytes())
48            .map_err(|e| e.to_string())?;
49        Ok(digest)
50    }
51
52    fn query_invariants(&self, scope: &str) -> Result<Vec<String>, String> {
53        let sparql = format!(
54            r#"SELECT ?inv WHERE {{
55                ?inv a <urn:lsp-max:andon:Invariant> ;
56                     <urn:lsp-max:andon:scope> "{scope}" .
57            }}"#
58        );
59        run_select_first_binding(&self.store, &sparql)
60    }
61
62    fn query_witnesses(&self, claim_id: &str) -> Result<Vec<String>, String> {
63        let sparql = format!(
64            r#"SELECT ?w WHERE {{
65                <{claim_id}> <urn:lsp-max:andon:hasWitness> ?w .
66            }}"#
67        );
68        run_select_first_binding(&self.store, &sparql)
69    }
70
71    fn query_repairs(&self, diagnostic_code: &str) -> Result<Vec<String>, String> {
72        let sparql = format!(
73            r#"SELECT ?repair WHERE {{
74                ?repair a <urn:lsp-max:andon:Repair> ;
75                        <urn:lsp-max:andon:forCode> "{diagnostic_code}" .
76            }}"#
77        );
78        run_select_first_binding(&self.store, &sparql)
79    }
80
81    /// # INVARIANT: OXIGRAPH_NOT_ON_HOT_PATH — lsif_import is cold-path only.
82    fn import_lsif_snapshot(
83        &self,
84        lsif_path: &std::path::Path,
85        graph_name: &str,
86    ) -> anyhow::Result<super::lsif_import::LsifImportStats> {
87        super::lsif_import::import_lsif_into_graph(&self.store, lsif_path, graph_name)
88    }
89}
90
91// ---------------------------------------------------------------------------
92// Private SPARQL helpers (oxigraph-aware; stay inside this module)
93// ---------------------------------------------------------------------------
94
95fn run_select_first_binding(store: &Store, sparql: &str) -> Result<Vec<String>, String> {
96    let evaluator = SparqlEvaluator::new();
97    let query = evaluator.parse_query(sparql).map_err(|e| e.to_string())?;
98    let results = query.on_store(store).execute().map_err(|e| e.to_string())?;
99
100    match results {
101        QueryResults::Solutions(sols) => {
102            let mut out = Vec::new();
103            for sol in sols {
104                let sol = sol.map_err(|e| e.to_string())?;
105                if let Some(term) = sol.get(0) {
106                    out.push(term_to_string(term));
107                }
108            }
109            Ok(out)
110        }
111        _ => Ok(Vec::new()),
112    }
113}
114
115/// Convert an Oxigraph `Term` to a plain `String`.
116/// RDF-star `Triple` terms are not supported and return an empty string.
117fn term_to_string(term: &Term) -> String {
118    match term {
119        Term::NamedNode(n) => n.as_str().to_string(),
120        Term::BlankNode(b) => b.as_str().to_string(),
121        Term::Literal(l) => l.value().to_string(),
122        _ => String::new(), // RDF-star Triple terms unsupported — OXIGRAPH_BOUNDARY_HELD
123    }
124}