bra0-kg 0.2.2

bra0 Knowledge Graph core — RDF transformations (sophia) + KgStore trait (NextGraph-First)
Documentation
//! Oxigraph implementation of KgStore — standalone mode
//!
//! Feature-gated behind `standalone`. Used for:
//! - Tauri desktop app (native oxigraph)
//! - topo CLI
//! - CI validation scripts
//!
//! In NextGraph mode, this module is NOT compiled.

use crate::store::{KgStore, StoreError, StoreResult};
use oxigraph::store::Store;
use oxigraph::io::RdfFormat;

pub struct OxigraphStore {
    store: Store,
}

impl OxigraphStore {
    pub fn new_memory() -> StoreResult<Self> {
        let store = Store::new()
            .map_err(|e| StoreError::LoadError(e.to_string()))?;
        Ok(Self { store })
    }

    /// Execute a raw SPARQL query, returning oxigraph QueryResults directly.
    /// Used by topo CLI for flexible result handling.
    pub fn raw_query(&self, sparql: &str) -> StoreResult<oxigraph::sparql::QueryResults> {
        self.store.query(sparql)
            .map_err(|e| StoreError::QueryError(e.to_string()))
    }

    /// Execute a SPARQL CONSTRUCT query. Returns result as N-Triples string.
    /// Used by OWL 2 RL materialization pipeline (owl.rs).
    pub fn sparql_construct(&self, sparql: &str) -> StoreResult<String> {
        use oxigraph::sparql::QueryResults;
        match self.store.query(sparql)
            .map_err(|e| StoreError::QueryError(e.to_string()))? {
            QueryResults::Graph(triples) => {
                let mut buf = Vec::new();
                for triple in triples {
                    let t = triple.map_err(|e| StoreError::QueryError(e.to_string()))?;
                    buf.extend_from_slice(format!("{} {} {} .\n",
                        t.subject, t.predicate, t.object).as_bytes());
                }
                String::from_utf8(buf)
                    .map_err(|e| StoreError::QueryError(e.to_string()))
            }
            _ => Err(StoreError::QueryError("Expected CONSTRUCT results (Graph)".into())),
        }
    }
}

impl KgStore for OxigraphStore {
    fn load_turtle(&mut self, turtle: &str, _graph_name: Option<&str>) -> StoreResult<usize> {
        let before = self.store.len().map_err(|e| StoreError::LoadError(e.to_string()))?;
        self.store
            .load_from_reader(RdfFormat::Turtle, turtle.as_bytes())
            .map_err(|e| StoreError::LoadError(e.to_string()))?;
        let after = self.store.len().map_err(|e| StoreError::LoadError(e.to_string()))?;
        Ok(after - before)
    }

    fn sparql_query(&self, sparql: &str) -> StoreResult<String> {
        use oxigraph::sparql::QueryResults;
        match self.store.query(sparql)
            .map_err(|e| StoreError::QueryError(e.to_string()))? {
            QueryResults::Solutions(solutions) => {
                let mut rows: Vec<Vec<String>> = Vec::new();
                for solution in solutions {
                    let s = solution.map_err(|e| StoreError::QueryError(e.to_string()))?;
                    let row: Vec<String> = s.values()
                        .iter()
                        .map(|v| match v {
                            Some(term) => term.to_string(),
                            None => String::new(),
                        })
                        .collect();
                    rows.push(row);
                }
                serde_json::to_string(&rows)
                    .map_err(|e| StoreError::QueryError(e.to_string()))
            }
            QueryResults::Boolean(b) => Ok(b.to_string()),
            QueryResults::Graph(_) => {
                Err(StoreError::QueryError("CONSTRUCT: use export_turtle instead".into()))
            }
        }
    }

    fn sparql_update(&mut self, sparql: &str) -> StoreResult<()> {
        self.store
            .update(sparql)
            .map_err(|e| StoreError::QueryError(e.to_string()))
    }

    fn export_turtle(&self, _graph_name: Option<&str>) -> StoreResult<String> {
        let mut buf = Vec::new();
        self.store
            .dump_to_writer(RdfFormat::Turtle, &mut buf)
            .map_err(|e| StoreError::ExportError(e.to_string()))?;
        String::from_utf8(buf)
            .map_err(|e| StoreError::ExportError(e.to_string()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn load_and_query() {
        let mut store = OxigraphStore::new_memory().unwrap();
        let ttl = r#"
            @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
            <http://example.org/A> a rdfs:Class ;
                rdfs:label "Test"@en .
        "#;
        let count = store.load_turtle(ttl, None).unwrap();
        assert_eq!(count, 2);

        let result = store.sparql_query(
            "SELECT ?s WHERE { ?s a <http://www.w3.org/2000/01/rdf-schema#Class> }"
        ).unwrap();
        assert!(result.contains("example.org/A"));
    }
}