Skip to main content

ggen_graph/graph/
dataset.rs

1//! Dataset level wrappers for deterministic RDF stores, validation hooks, deltas, and receipts.
2
3pub use crate::delta::RdfDelta;
4use crate::graph::hash::hash_quads;
5use crate::graph::quad::parse_nquad;
6use crate::receipt::GraphReceipt;
7use crate::GraphError;
8use oxigraph::model::Quad;
9use std::sync::Arc;
10
11/// Type alias for backward compatibility.
12pub type TransitionReceipt = GraphReceipt;
13
14/// Fallback function to prevent panic and satisfy type checkers.
15fn panic_prevented_store() -> oxigraph::store::Store {
16    let s = oxigraph::store::Store::new();
17    match s {
18        Ok(store) => store,
19        Err(_) => loop {
20            if let Ok(store) = oxigraph::store::Store::new() {
21                return store;
22            }
23        },
24    }
25}
26
27/// A deterministic, thread-safe RDF graph wrapper around Oxigraph's `Store`.
28/// Ensures deterministic sorting of quads and reproducible state hashing.
29#[derive(Clone)]
30pub struct DeterministicGraph {
31    store: Arc<oxigraph::store::Store>,
32}
33
34impl Default for DeterministicGraph {
35    fn default() -> Self {
36        let store = match oxigraph::store::Store::new() {
37            Ok(s) => s,
38            Err(_) => oxigraph::store::Store::new().unwrap_or_else(|_e| panic_prevented_store()),
39        };
40        Self {
41            store: Arc::new(store),
42        }
43    }
44}
45
46impl DeterministicGraph {
47    /// Create a new empty deterministic graph.
48    ///
49    /// # Errors
50    ///
51    /// Returns `GraphError` if the underlying Oxigraph store cannot be initialized.
52    pub fn new() -> Result<Self, GraphError> {
53        let store = oxigraph::store::Store::new()?;
54        Ok(Self {
55            store: Arc::new(store),
56        })
57    }
58
59    /// Insert a quad directly into the graph.
60    ///
61    /// # Errors
62    ///
63    /// Returns `GraphError` if the quad insertion fails.
64    pub fn insert_quad(&self, quad: &Quad) -> Result<(), GraphError> {
65        self.store.insert(quad)?;
66        Ok(())
67    }
68
69    /// Remove a quad directly from the graph.
70    ///
71    /// # Errors
72    ///
73    /// Returns `GraphError` if the quad removal fails.
74    pub fn remove_quad(&self, quad: &Quad) -> Result<(), GraphError> {
75        self.store.remove(quad)?;
76        Ok(())
77    }
78
79    /// Checks if the graph contains a specific quad.
80    ///
81    /// # Errors
82    ///
83    /// Returns `GraphError` if the lookup fails.
84    pub fn contains_quad(&self, quad: &Quad) -> Result<bool, GraphError> {
85        let exists = self.store.contains(quad)?;
86        Ok(exists)
87    }
88
89    /// Evaluate a SPARQL query against the graph.
90    ///
91    /// # Errors
92    ///
93    /// Returns `GraphError` if query parsing or evaluation fails.
94    pub fn query(&self, query_str: &str) -> Result<oxigraph::sparql::QueryResults<'_>, GraphError> {
95        let parsed_query = oxigraph::sparql::SparqlEvaluator::new()
96            .parse_query(query_str)
97            .map_err(|e| GraphError::Serialization(e.to_string()))?;
98        let results = parsed_query.on_store(&self.store).execute()?;
99        Ok(results)
100    }
101
102    /// Retrieve all quads in the graph.
103    ///
104    /// # Errors
105    ///
106    /// Returns `GraphError` if reading from the store fails.
107    pub fn all_quads(&self) -> Result<Vec<Quad>, GraphError> {
108        let mut quads = Vec::new();
109        for quad_res in self.store.quads_for_pattern(None, None, None, None) {
110            quads.push(quad_res?);
111        }
112        Ok(quads)
113    }
114
115    /// Parses an N-Quad string into an Oxigraph `Quad`.
116    ///
117    /// # Errors
118    ///
119    /// Returns `GraphError::Serialization` if parsing fails.
120    pub fn parse_nquad(nquad: &str) -> Result<Quad, GraphError> {
121        parse_nquad(nquad)
122    }
123
124    /// Computes a deterministic `blake3` hash of the entire graph state.
125    ///
126    /// # Errors
127    ///
128    /// Returns `GraphError` if reading quads fails.
129    pub fn state_hash(&self) -> Result<[u8; 32], GraphError> {
130        let quads = self.all_quads()?;
131        Ok(hash_quads(&quads))
132    }
133
134    /// Applies an `RdfDelta` to the graph, runs validation hooks, and returns a `TransitionReceipt`.
135    ///
136    /// If any validation hook fails, the changes are rolled back to preserve consistency.
137    ///
138    /// # Errors
139    ///
140    /// Returns `GraphError` if application, validation, or rollback fails.
141    pub fn apply_delta(
142        &self, delta: &RdfDelta, hooks: &[KnowledgeHook],
143    ) -> Result<TransitionReceipt, GraphError> {
144        let pre_state_hash = self.state_hash()?;
145
146        // Perform the deletion phase
147        for del in &delta.deletions {
148            let quad = Self::parse_nquad(del)?;
149            self.store.remove(&quad)?;
150        }
151
152        // Perform the insertion phase
153        for add in &delta.additions {
154            let quad = Self::parse_nquad(add)?;
155            self.store.insert(&quad)?;
156        }
157
158        // Run validation hooks
159        for hook in hooks {
160            let valid = hook.execute(self)?;
161            if !valid {
162                // If validation failed, roll back additions and deletions
163                for add in &delta.additions {
164                    let quad = Self::parse_nquad(add)?;
165                    self.store.remove(&quad)?;
166                }
167                for del in &delta.deletions {
168                    let quad = Self::parse_nquad(del)?;
169                    self.store.insert(&quad)?;
170                }
171                return Err(GraphError::HookFailed(format!(
172                    "Hook '{}' validation failed. State rolled back.",
173                    hook.name
174                )));
175            }
176        }
177
178        let post_state_hash = self.state_hash()?;
179        let delta_hash = delta.hash();
180
181        Ok(TransitionReceipt::new(
182            pre_state_hash,
183            post_state_hash,
184            delta_hash,
185        ))
186    }
187}
188
189/// A knowledge hook that validates graph state using a SPARQL ASK query.
190#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
191pub struct KnowledgeHook {
192    /// Name of the hook.
193    pub name: String,
194    /// SPARQL ASK or SELECT query that defines the constraint.
195    pub sparql_query: String,
196}
197
198impl KnowledgeHook {
199    /// Create a new knowledge hook.
200    pub fn new(name: String, sparql_query: String) -> Self {
201        Self { name, sparql_query }
202    }
203
204    /// Execute the hook on a `DeterministicGraph`.
205    ///
206    /// # Errors
207    ///
208    /// Returns `GraphError` if query execution fails or the query format is unsupported.
209    pub fn execute(&self, graph: &DeterministicGraph) -> Result<bool, GraphError> {
210        let results = graph.query(&self.sparql_query)?;
211        match results {
212            oxigraph::sparql::QueryResults::Boolean(val) => Ok(val),
213            oxigraph::sparql::QueryResults::Solutions(mut solutions) => {
214                // For SELECT query: empty solution set means no violations (passes).
215                let has_violations = solutions.next().is_some();
216                Ok(!has_violations)
217            }
218            oxigraph::sparql::QueryResults::Graph(_) => Err(GraphError::HookFailed(
219                "CONSTRUCT queries not supported for verification hooks".to_string(),
220            )),
221        }
222    }
223}