Skip to main content

pyshacl_turbo_engine/
graph_views.rs

1//! Borrowed logical dataset views; neither view materializes the base relation.
2use oxigraph::model::*;
3
4use crate::{Error, GraphRead, Result};
5
6type Matches<'a> = Box<dyn Iterator<Item = Result<Quad>> + 'a>;
7
8/// Expose the union as the default graph while preserving original named graphs.
9/// The source must remain consistent throughout a read, as a borrowed Dataset
10/// or native transaction does. A repeated exact match must select the same
11/// first quad; no sorted order is required.
12pub struct UnionGraph<'a, G: GraphRead + ?Sized> {
13    source: &'a G,
14}
15
16impl<'a, G: GraphRead + ?Sized> UnionGraph<'a, G> {
17    /// Borrow a consistent source; do not clone or snapshot its base data.
18    pub fn new(source: &'a G) -> Self {
19        Self { source }
20    }
21
22    fn default_matches<'b>(
23        &'b self,
24        subject: Option<NamedOrBlankNodeRef<'_>>,
25        predicate: Option<NamedNodeRef<'_>>,
26        object: Option<TermRef<'_>>,
27    ) -> impl Iterator<Item = Result<Quad>> + use<'a, 'b, G> {
28        self.source
29            .matching_quads(subject, predicate, object, None)
30            .filter_map(|quad| match quad {
31                Err(error) => Some(Err(error)),
32                Ok(mut quad) => match self.representative(&quad) {
33                    Err(error) => Some(Err(error)),
34                    Ok(false) => None,
35                    Ok(true) => {
36                        quad.graph_name = GraphName::DefaultGraph;
37                        Some(Ok(quad))
38                    }
39                },
40            })
41    }
42
43    fn representative(&self, quad: &Quad) -> Result<bool> {
44        // An exact indexed lookup identifies one context without retaining a
45        // dataset-sized visited set. Memory storage need not yield sorted quads.
46        let first = self
47            .source
48            .matching_quads(
49                Some(quad.subject.as_ref()),
50                Some(quad.predicate.as_ref()),
51                Some(quad.object.as_ref()),
52                None,
53            )
54            .next()
55            .transpose()?
56            .ok_or_else(|| Error::Runtime("Graph changed during union read".into()))?;
57        Ok(first.graph_name == quad.graph_name)
58    }
59}
60
61impl<'source, G: GraphRead + ?Sized> GraphRead for UnionGraph<'source, G> {
62    fn matching_quads<'a>(
63        &'a self,
64        subject: Option<NamedOrBlankNodeRef<'_>>,
65        predicate: Option<NamedNodeRef<'_>>,
66        object: Option<TermRef<'_>>,
67        graph: Option<GraphNameRef<'_>>,
68    ) -> impl Iterator<Item = Result<Quad>> + use<'a, 'source, G> {
69        let matches: Matches<'a> = match graph {
70            Some(GraphNameRef::DefaultGraph) => {
71                Box::new(self.default_matches(subject, predicate, object))
72            }
73            Some(_) => Box::new(
74                self.source
75                    .matching_quads(subject, predicate, object, graph),
76            ),
77            None => Box::new(
78                self.default_matches(subject, predicate, object).chain(
79                    self.source
80                        .matching_quads(subject, predicate, object, None)
81                        .filter(|q| match q {
82                            Ok(q) => !q.graph_name.is_default_graph(),
83                            Err(_) => true,
84                        }),
85                ),
86            ),
87        };
88        matches
89    }
90
91    fn named_graphs(&self) -> impl Iterator<Item = Result<NamedOrBlankNode>> + '_ {
92        self.source.named_graphs()
93    }
94
95    fn contains_named_graph(&self, graph: &NamedOrBlankNode) -> Result<bool> {
96        self.source.contains_named_graph(graph)
97    }
98}
99
100/// Add a separate named shapes graph without cloning or modifying the base.
101/// Shapes are projected from their union; their original contexts do not leak
102/// into the base dataset. The reserved graph exists even when shapes are empty.
103pub struct OverlayGraph<'a, G: GraphRead + ?Sized, S: GraphRead + ?Sized> {
104    source: &'a G,
105    shapes: UnionGraph<'a, S>,
106    graph: GraphName,
107}
108
109impl<'a, G: GraphRead + ?Sized, S: GraphRead + ?Sized> OverlayGraph<'a, G, S> {
110    /// Reserve a fresh named graph; collisions, including empty graphs, fail.
111    pub fn new(source: &'a G, shapes: &'a S, graph: NamedOrBlankNode) -> Result<Self> {
112        if source.contains_named_graph(&graph)? {
113            return Err(Error::Invalid(format!(
114                "Overlay graph already exists: {graph}"
115            )));
116        }
117        Ok(Self {
118            source,
119            shapes: UnionGraph::new(shapes),
120            graph: graph.into(),
121        })
122    }
123
124    fn overlay_matches<'b>(
125        &'b self,
126        subject: Option<NamedOrBlankNodeRef<'_>>,
127        predicate: Option<NamedNodeRef<'_>>,
128        object: Option<TermRef<'_>>,
129    ) -> impl Iterator<Item = Result<Quad>> + use<'a, 'b, G, S> {
130        self.shapes
131            .matching_quads(subject, predicate, object, Some(GraphNameRef::DefaultGraph))
132            .map(|quad| {
133                quad.map(|mut quad| {
134                    quad.graph_name = self.graph.clone();
135                    quad
136                })
137            })
138    }
139}
140
141impl<'source, G: GraphRead + ?Sized, S: GraphRead + ?Sized> GraphRead
142    for OverlayGraph<'source, G, S>
143{
144    fn matching_quads<'a>(
145        &'a self,
146        subject: Option<NamedOrBlankNodeRef<'_>>,
147        predicate: Option<NamedNodeRef<'_>>,
148        object: Option<TermRef<'_>>,
149        graph: Option<GraphNameRef<'_>>,
150    ) -> impl Iterator<Item = Result<Quad>> + use<'a, 'source, G, S> {
151        let matches: Matches<'a> = match graph {
152            Some(graph) if graph == self.graph.as_ref() => {
153                Box::new(self.overlay_matches(subject, predicate, object))
154            }
155            Some(_) => Box::new(
156                self.source
157                    .matching_quads(subject, predicate, object, graph),
158            ),
159            None => Box::new(
160                self.source
161                    .matching_quads(subject, predicate, object, None)
162                    .chain(self.overlay_matches(subject, predicate, object)),
163            ),
164        };
165        matches
166    }
167
168    fn named_graphs(&self) -> impl Iterator<Item = Result<NamedOrBlankNode>> + '_ {
169        self.source
170            .named_graphs()
171            .chain(std::iter::once(Ok(match &self.graph {
172                GraphName::NamedNode(node) => node.clone().into(),
173                GraphName::BlankNode(node) => node.clone().into(),
174                GraphName::DefaultGraph => unreachable!("overlay reserves a named graph"),
175            })))
176    }
177
178    fn contains_named_graph(&self, graph: &NamedOrBlankNode) -> Result<bool> {
179        Ok(self.graph.as_ref() == GraphNameRef::from(graph.as_ref())
180            || self.source.contains_named_graph(graph)?)
181    }
182}