ggen-graph 26.6.11

Deterministic graph module for ggen
Documentation
//! Dataset level wrappers for deterministic RDF stores, validation hooks, deltas, and receipts.

pub use crate::delta::RdfDelta;
use crate::graph::hash::hash_quads;
use crate::graph::quad::parse_nquad;
use crate::receipt::GraphReceipt;
use crate::GraphError;
use oxigraph::model::Quad;
use std::sync::Arc;

/// Type alias for backward compatibility.
pub type TransitionReceipt = GraphReceipt;

/// Fallback function to prevent panic and satisfy type checkers.
fn panic_prevented_store() -> oxigraph::store::Store {
    let s = oxigraph::store::Store::new();
    match s {
        Ok(store) => store,
        Err(_) => loop {
            if let Ok(store) = oxigraph::store::Store::new() {
                return store;
            }
        },
    }
}

/// A deterministic, thread-safe RDF graph wrapper around Oxigraph's `Store`.
/// Ensures deterministic sorting of quads and reproducible state hashing.
#[derive(Clone)]
pub struct DeterministicGraph {
    store: Arc<oxigraph::store::Store>,
}

impl Default for DeterministicGraph {
    fn default() -> Self {
        let store = match oxigraph::store::Store::new() {
            Ok(s) => s,
            Err(_) => oxigraph::store::Store::new().unwrap_or_else(|_e| panic_prevented_store()),
        };
        Self {
            store: Arc::new(store),
        }
    }
}

impl DeterministicGraph {
    /// Create a new empty deterministic graph.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if the underlying Oxigraph store cannot be initialized.
    pub fn new() -> Result<Self, GraphError> {
        let store = oxigraph::store::Store::new()?;
        Ok(Self {
            store: Arc::new(store),
        })
    }

    /// Insert a quad directly into the graph.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if the quad insertion fails.
    pub fn insert_quad(&self, quad: &Quad) -> Result<(), GraphError> {
        self.store.insert(quad)?;
        Ok(())
    }

    /// Remove a quad directly from the graph.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if the quad removal fails.
    pub fn remove_quad(&self, quad: &Quad) -> Result<(), GraphError> {
        self.store.remove(quad)?;
        Ok(())
    }

    /// Checks if the graph contains a specific quad.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if the lookup fails.
    pub fn contains_quad(&self, quad: &Quad) -> Result<bool, GraphError> {
        let exists = self.store.contains(quad)?;
        Ok(exists)
    }

    /// Evaluate a SPARQL query against the graph.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if query parsing or evaluation fails.
    pub fn query(&self, query_str: &str) -> Result<oxigraph::sparql::QueryResults<'_>, GraphError> {
        let parsed_query = oxigraph::sparql::SparqlEvaluator::new()
            .parse_query(query_str)
            .map_err(|e| GraphError::Serialization(e.to_string()))?;
        let results = parsed_query.on_store(&self.store).execute()?;
        Ok(results)
    }

    /// Retrieve all quads in the graph.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if reading from the store fails.
    pub fn all_quads(&self) -> Result<Vec<Quad>, GraphError> {
        let mut quads = Vec::new();
        for quad_res in self.store.quads_for_pattern(None, None, None, None) {
            quads.push(quad_res?);
        }
        Ok(quads)
    }

    /// Parses an N-Quad string into an Oxigraph `Quad`.
    ///
    /// # Errors
    ///
    /// Returns `GraphError::Serialization` if parsing fails.
    pub fn parse_nquad(nquad: &str) -> Result<Quad, GraphError> {
        parse_nquad(nquad)
    }

    /// Computes a deterministic `blake3` hash of the entire graph state.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if reading quads fails.
    pub fn state_hash(&self) -> Result<[u8; 32], GraphError> {
        let quads = self.all_quads()?;
        Ok(hash_quads(&quads))
    }

    /// Applies an `RdfDelta` to the graph, runs validation hooks, and returns a `TransitionReceipt`.
    ///
    /// If any validation hook fails, the changes are rolled back to preserve consistency.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if application, validation, or rollback fails.
    pub fn apply_delta(
        &self, delta: &RdfDelta, hooks: &[KnowledgeHook],
    ) -> Result<TransitionReceipt, GraphError> {
        let pre_state_hash = self.state_hash()?;

        // Perform the deletion phase
        for del in &delta.deletions {
            let quad = Self::parse_nquad(del)?;
            self.store.remove(&quad)?;
        }

        // Perform the insertion phase
        for add in &delta.additions {
            let quad = Self::parse_nquad(add)?;
            self.store.insert(&quad)?;
        }

        // Run validation hooks
        for hook in hooks {
            let valid = hook.execute(self)?;
            if !valid {
                // If validation failed, roll back additions and deletions
                for add in &delta.additions {
                    let quad = Self::parse_nquad(add)?;
                    self.store.remove(&quad)?;
                }
                for del in &delta.deletions {
                    let quad = Self::parse_nquad(del)?;
                    self.store.insert(&quad)?;
                }
                return Err(GraphError::HookFailed(format!(
                    "Hook '{}' validation failed. State rolled back.",
                    hook.name
                )));
            }
        }

        let post_state_hash = self.state_hash()?;
        let delta_hash = delta.hash();

        Ok(TransitionReceipt::new(
            pre_state_hash,
            post_state_hash,
            delta_hash,
        ))
    }
}

/// A knowledge hook that validates graph state using a SPARQL ASK query.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KnowledgeHook {
    /// Name of the hook.
    pub name: String,
    /// SPARQL ASK or SELECT query that defines the constraint.
    pub sparql_query: String,
}

impl KnowledgeHook {
    /// Create a new knowledge hook.
    pub fn new(name: String, sparql_query: String) -> Self {
        Self { name, sparql_query }
    }

    /// Execute the hook on a `DeterministicGraph`.
    ///
    /// # Errors
    ///
    /// Returns `GraphError` if query execution fails or the query format is unsupported.
    pub fn execute(&self, graph: &DeterministicGraph) -> Result<bool, GraphError> {
        let results = graph.query(&self.sparql_query)?;
        match results {
            oxigraph::sparql::QueryResults::Boolean(val) => Ok(val),
            oxigraph::sparql::QueryResults::Solutions(mut solutions) => {
                // For SELECT query: empty solution set means no violations (passes).
                let has_violations = solutions.next().is_some();
                Ok(!has_violations)
            }
            oxigraph::sparql::QueryResults::Graph(_) => Err(GraphError::HookFailed(
                "CONSTRUCT queries not supported for verification hooks".to_string(),
            )),
        }
    }
}