Skip to main content

pyshacl_turbo_engine/
graph_read.rs

1//! Borrowed indexed reads shared by dataset and transaction-backed traversal.
2use oxigraph::model::*;
3use oxigraph::store::Transaction;
4
5use crate::{Error, Result};
6
7/// A fallible stream of stored quads, with explicit graph matching.
8///
9/// `None` matches every graph; `Some(DefaultGraph)` matches only the default
10/// graph. Implementations preserve stored RDF terms and graph contexts. This
11/// interface neither constructs a graph union nor copies the base dataset.
12/// Storage owners remain responsible for exact-term encoding: a reader cannot
13/// recover lexical forms already normalized by its storage backend.
14/// During a consistent read, identical patterns must yield the same first quad;
15/// no lexical sorting is required. Borrowed views use this stable representative
16/// to deduplicate graph contexts without retaining all previously visited triples.
17pub trait GraphRead {
18    /// Stream matching quads. Read failures must remain errors, never omissions.
19    /// Retain the source borrow, not the caller's temporary pattern references.
20    fn matching_quads<'a>(
21        &'a self,
22        subject: Option<NamedOrBlankNodeRef<'_>>,
23        predicate: Option<NamedNodeRef<'_>>,
24        object: Option<TermRef<'_>>,
25        graph: Option<GraphNameRef<'_>>,
26    ) -> impl Iterator<Item = Result<Quad>> + use<'a, Self>;
27
28    /// Stream each named graph exactly once, including empty backend graphs.
29    /// Iteration must not materialize the set of graph names.
30    fn named_graphs(&self) -> impl Iterator<Item = Result<NamedOrBlankNode>> + '_;
31
32    /// Test graph existence, including empty graphs in capable storage backends.
33    fn contains_named_graph(&self, graph: &NamedOrBlankNode) -> Result<bool>;
34}
35
36impl GraphRead for Dataset {
37    fn matching_quads<'a>(
38        &'a self,
39        subject: Option<NamedOrBlankNodeRef<'_>>,
40        predicate: Option<NamedNodeRef<'_>>,
41        object: Option<TermRef<'_>>,
42        graph: Option<GraphNameRef<'_>>,
43    ) -> impl Iterator<Item = Result<Quad>> + use<'a> {
44        // bounded: at most four owned pattern terms, independent of source size.
45        let predicate = predicate.map(NamedNodeRef::into_owned);
46        let object = object.map(TermRef::into_owned);
47        let graph = graph.map(GraphNameRef::into_owned);
48        let candidates: Box<dyn Iterator<Item = QuadRef<'a>> + 'a> = if let Some(subject) = subject
49        {
50            Box::new(self.quads_for_subject(subject))
51        } else if let Some(object) = object.as_ref() {
52            Box::new(self.quads_for_object(object.as_ref()))
53        } else if let Some(predicate) = predicate.as_ref() {
54            Box::new(self.quads_for_predicate(predicate.as_ref()))
55        } else if let Some(graph) = graph.as_ref() {
56            Box::new(self.quads_for_graph_name(graph.as_ref()))
57        } else {
58            Box::new(self.iter())
59        };
60        candidates
61            .filter(move |q| {
62                predicate.as_ref().is_none_or(|p| p.as_ref() == q.predicate)
63                    && object.as_ref().is_none_or(|o| o.as_ref() == q.object)
64                    && graph.as_ref().is_none_or(|g| g.as_ref() == q.graph_name)
65            })
66            .map(|quad| Ok(quad.into_owned()))
67    }
68
69    fn named_graphs(&self) -> impl Iterator<Item = Result<NamedOrBlankNode>> + '_ {
70        // Select one indexed representative per graph; retain no graph-name set.
71        self.iter().filter_map(|quad| {
72            let name = match quad.graph_name {
73                GraphNameRef::NamedNode(node) => NamedOrBlankNode::from(node.into_owned()),
74                GraphNameRef::BlankNode(node) => NamedOrBlankNode::from(node.into_owned()),
75                GraphNameRef::DefaultGraph => return None,
76            };
77            (self.quads_for_graph_name(quad.graph_name).next() == Some(quad)).then_some(Ok(name))
78        })
79    }
80    fn contains_named_graph(&self, graph: &NamedOrBlankNode) -> Result<bool> {
81        Ok(self.quads_for_graph_name(graph.as_ref()).next().is_some())
82    }
83}
84
85impl<'tx> GraphRead for Transaction<'tx> {
86    fn matching_quads<'a>(
87        &'a self,
88        subject: Option<NamedOrBlankNodeRef<'_>>,
89        predicate: Option<NamedNodeRef<'_>>,
90        object: Option<TermRef<'_>>,
91        graph: Option<GraphNameRef<'_>>,
92    ) -> impl Iterator<Item = Result<Quad>> + use<'a, 'tx> {
93        self.quads_for_pattern(subject, predicate, object, graph)
94            .map(|quad| {
95                quad.map_err(|error| Error::Runtime(format!("Storage read failed: {error}")))
96            })
97    }
98
99    fn named_graphs(&self) -> impl Iterator<Item = Result<NamedOrBlankNode>> + '_ {
100        Transaction::named_graphs(self).map(|graph| {
101            graph.map_err(|error| Error::Runtime(format!("Storage read failed: {error}")))
102        })
103    }
104
105    fn contains_named_graph(&self, graph: &NamedOrBlankNode) -> Result<bool> {
106        Transaction::contains_named_graph(self, graph.as_ref())
107            .map_err(|error| Error::Runtime(format!("Storage read failed: {error}")))
108    }
109}