Skip to main content

vertep_store/
sparql.rs

1//! SPARQL text for graph store writes, and conversion of evaluator results.
2//!
3//! Backends that speak SPARQL Update (a remote endpoint, tiramemsu) write quads as
4//! `INSERT DATA` and `DELETE DATA` requests built here. Terms are written in their N-Triples
5//! form, which is valid SPARQL 1.2 (triple terms as `<<( s p o )>>`).
6
7use std::collections::BTreeMap;
8use std::fmt::Write;
9
10use oxrdf::{GraphName, NamedNode, NamedOrBlankNode, Quad, Term};
11
12use crate::error::StoreError;
13use crate::graph::{QueryResults, Solutions};
14
15/// Groups quads by graph, keeping the order of first appearance within each graph.
16fn by_graph(quads: &[Quad]) -> BTreeMap<String, (GraphName, Vec<&Quad>)> {
17    let mut groups: BTreeMap<String, (GraphName, Vec<&Quad>)> = BTreeMap::new();
18    for q in quads {
19        groups
20            .entry(q.graph_name.to_string())
21            .or_insert_with(|| (q.graph_name.clone(), Vec::new()))
22            .1
23            .push(q);
24    }
25    groups
26}
27
28fn write_block(out: &mut String, quads: &[Quad]) {
29    for (_, (g, qs)) in by_graph(quads) {
30        let indent = match &g {
31            GraphName::DefaultGraph => "  ",
32            _ => {
33                let _ = writeln!(out, "  GRAPH {g} {{");
34                "    "
35            }
36        };
37        for q in qs {
38            let _ = writeln!(out, "{indent}{} {} {} .", q.subject, q.predicate, q.object);
39        }
40        if !matches!(g, GraphName::DefaultGraph) {
41            out.push_str("  }\n");
42        }
43    }
44}
45
46/// `INSERT DATA { ... }` for `quads`.
47pub fn insert_data(quads: &[Quad]) -> String {
48    let mut out = String::from("INSERT DATA {\n");
49    write_block(&mut out, quads);
50    out.push('}');
51    out
52}
53
54fn has_blank(q: &Quad) -> bool {
55    matches!(q.subject, NamedOrBlankNode::BlankNode(_))
56        || term_has_blank(&q.object)
57        || matches!(q.graph_name, GraphName::BlankNode(_))
58}
59
60fn term_has_blank(t: &Term) -> bool {
61    match t {
62        Term::BlankNode(_) => true,
63        Term::Triple(tr) => {
64            matches!(tr.subject, NamedOrBlankNode::BlankNode(_)) || term_has_blank(&tr.object)
65        }
66        _ => false,
67    }
68}
69
70/// `DELETE DATA { ... }` for `quads`.
71///
72/// # Errors
73///
74/// [`StoreError::Invalid`] when a quad contains a blank node, which `DELETE DATA` cannot name.
75pub fn delete_data(quads: &[Quad]) -> Result<String, StoreError> {
76    if quads.iter().any(has_blank) {
77        return Err(StoreError::invalid(
78            "blank nodes cannot be named for removal",
79        ));
80    }
81    let mut out = String::from("DELETE DATA {\n");
82    write_block(&mut out, quads);
83    out.push('}');
84    Ok(out)
85}
86
87/// `CLEAR SILENT GRAPH <graph>`.
88pub fn clear_graph(graph: &NamedNode) -> String {
89    format!("CLEAR SILENT GRAPH {graph}")
90}
91
92/// Collects a `spareval` result (what Oxigraph and the in-memory reference store return) into
93/// [`QueryResults`].
94///
95/// # Errors
96///
97/// [`StoreError::Backend`] when evaluation fails while iterating.
98pub fn collect_results(results: spareval::QueryResults<'_>) -> Result<QueryResults, StoreError> {
99    match results {
100        spareval::QueryResults::Boolean(b) => Ok(QueryResults::Boolean(b)),
101        spareval::QueryResults::Solutions(iter) => {
102            let variables = iter
103                .variables()
104                .iter()
105                .map(|v| v.as_str().to_owned())
106                .collect();
107            let mut rows = Vec::new();
108            for s in iter {
109                let s = s.map_err(StoreError::backend)?;
110                rows.push(s.values().to_vec());
111            }
112            Ok(QueryResults::Solutions(Solutions { variables, rows }))
113        }
114        spareval::QueryResults::Graph(iter) => {
115            let mut triples = Vec::new();
116            for t in iter {
117                triples.push(t.map_err(StoreError::backend)?);
118            }
119            Ok(QueryResults::Graph(triples))
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use oxrdf::{BlankNode, Literal, Triple};
128
129    fn n(s: &str) -> NamedNode {
130        NamedNode::new(format!("urn:t:{s}")).unwrap()
131    }
132
133    #[test]
134    fn insert_data_groups_by_graph() {
135        let quads = vec![
136            Quad::new(n("a"), n("p"), Literal::from("x\"y"), n("g")),
137            Quad::new(n("a"), n("p"), n("b"), GraphName::DefaultGraph),
138            Quad::new(
139                n("r"),
140                n("reifies"),
141                Term::Triple(Box::new(Triple::new(n("a"), n("p"), n("b")))),
142                n("g"),
143            ),
144        ];
145        let text = insert_data(&quads);
146        assert!(text.starts_with("INSERT DATA {"));
147        assert!(text.contains("GRAPH <urn:t:g> {"));
148        assert!(text.contains(r#"<urn:t:a> <urn:t:p> "x\"y" ."#));
149        assert!(text.contains("<<( <urn:t:a> <urn:t:p> <urn:t:b> )>>"));
150        spargebra::SparqlParser::new()
151            .parse_update(&text)
152            .expect("valid SPARQL 1.2 update");
153    }
154
155    #[test]
156    fn delete_data_refuses_blank_nodes() {
157        let q = Quad::new(BlankNode::default(), n("p"), n("o"), n("g"));
158        assert!(delete_data(&[q]).is_err());
159        let ok = delete_data(&[Quad::new(n("s"), n("p"), n("o"), n("g"))]).unwrap();
160        spargebra::SparqlParser::new().parse_update(&ok).unwrap();
161        spargebra::SparqlParser::new()
162            .parse_update(&clear_graph(&n("g")))
163            .unwrap();
164    }
165}