1use crate::encoding::{decode_inline, decode_row, named_node_id, Tag, INT_OFFSET, PAYLOAD_BITS};
13use crate::error::Result;
14use crate::registry::{scoped_quads, SchemaRole};
15use crate::sql::{col, Capabilities, Request, Response, Statement};
16use oxrdf::vocab::rdf;
17use oxrdf::{NamedNode, QuadRef, Term};
18use spargebra::term::NamedNodePattern;
19use spargebra::{GraphUpdateOperation, Update};
20use std::collections::BTreeMap;
21
22const SH: &str = "http://www.w3.org/ns/shacl#";
23
24const SHAPE_LOCALS: [&str; 10] = [
31 "targetClass",
32 "property",
33 "path",
34 "datatype",
35 "minCount",
36 "maxCount",
37 "pattern",
38 "class",
39 "node",
40 "in",
41];
42
43fn sh(local: &str) -> i64 {
44 named_node_id(&format!("{SH}{local}"))
45}
46
47pub fn is_shape_quad(q: QuadRef<'_>) -> bool {
49 crate::registry::is_registry_quad(q) || is_shape_iri(q.predicate.as_str())
51}
52
53fn is_shape_iri(p: &str) -> bool {
54 p.strip_prefix(SH)
55 .is_some_and(|l| SHAPE_LOCALS.contains(&l))
56}
57
58pub fn update_touches_shapes(update: &Update) -> bool {
60 crate::registry::update_touches_registry(update) || touches_shape_triples(update)
61}
62
63fn touches_shape_triples(update: &Update) -> bool {
64 let pattern = |p: &NamedNodePattern| match p {
65 NamedNodePattern::Variable(_) => true,
66 NamedNodePattern::NamedNode(n) => is_shape_iri(n.as_str()),
67 };
68 update.operations.iter().any(|op| match op {
69 GraphUpdateOperation::InsertData { data } => {
70 data.iter().any(|q| is_shape_iri(q.predicate.as_str()))
71 }
72 GraphUpdateOperation::DeleteData { data } => {
73 data.iter().any(|q| is_shape_iri(q.predicate.as_str()))
74 }
75 GraphUpdateOperation::DeleteInsert { delete, insert, .. } => {
76 delete.iter().any(|q| pattern(&q.predicate))
77 || insert.iter().any(|q| pattern(&q.predicate))
78 }
79 GraphUpdateOperation::Create { .. } => false,
80 GraphUpdateOperation::Load { .. }
81 | GraphUpdateOperation::Clear { .. }
82 | GraphUpdateOperation::Drop { .. } => true,
83 })
84}
85
86fn int_value(x: &str) -> String {
90 let int_base = Tag::Integer.base() + INT_OFFSET;
91 let k_int = Tag::Integer as i64;
92 format!(
93 "CASE WHEN (({x}) >> {PAYLOAD_BITS}) = {k_int} THEN ({x}) - {int_base} \
94 ELSE (SELECT CAST(n.num AS INTEGER) FROM terms n WHERE n.id = ({x}) AND n.nt IS NOT NULL) END"
95 )
96}
97
98pub fn refresh_statements() -> Vec<Statement> {
100 let sq = scoped_quads(SchemaRole::Shacl);
101 let ps = format!(
103 "SELECT t.o AS target, pr.o AS pshape, pa.o AS path \
104 FROM {sq} t JOIN {sq} pr ON pr.s = t.s AND pr.p = {property} \
105 JOIN {sq} pa ON pa.s = pr.o AND pa.p = {path} WHERE t.p = {target_class}",
106 property = sh("property"),
107 path = sh("path"),
108 target_class = sh("targetClass"),
109 );
110 let lex_of = |local: &str| {
112 format!(
113 "(SELECT v.lex FROM {sq} x JOIN terms v ON v.id = x.o WHERE x.s = ps.pshape AND x.p = {})",
114 sh(local)
115 )
116 };
117 let int_of = |local: &str| {
118 format!(
119 "(SELECT {} FROM {sq} x WHERE x.s = ps.pshape AND x.p = {})",
120 int_value("x.o"),
121 sh(local)
122 )
123 };
124 let rel = format!(
126 "(SELECT EXISTS (SELECT 1 FROM {sq} x WHERE x.s = ps.pshape AND x.p IN ({}, {})))",
127 sh("class"),
128 sh("node")
129 );
130 let index = format!(
133 "INSERT OR REPLACE INTO shapes_index(target, path, datatype, min_count, max_count, pattern, relationship) \
134 SELECT tt.lex, pt.lex, MAX({datatype}), MAX({min}), MAX({max}), MAX({pattern}), MAX({rel}) \
135 FROM ({ps}) ps JOIN terms tt ON tt.id = ps.target JOIN terms pt ON pt.id = ps.path \
136 GROUP BY tt.lex, pt.lex",
137 datatype = lex_of("datatype"),
138 min = int_of("minCount"),
139 max = int_of("maxCount"),
140 pattern = lex_of("pattern"),
141 );
142 let values = format!(
144 "WITH RECURSIVE cells(pshape, node) AS (\
145 SELECT x.s, x.o FROM {sq} x WHERE x.p = {sh_in} \
146 UNION SELECT c.pshape, r.o FROM cells c JOIN {sq} r ON r.s = c.node AND r.p = {rest}) \
147 INSERT OR REPLACE INTO shapes_in(target, path, id, lex, dt, lang, dir) \
148 SELECT tt.lex, pt.lex, f.o, v.lex, v.dt, v.lang, v.dir \
149 FROM ({ps}) ps JOIN cells c ON c.pshape = ps.pshape \
150 JOIN {sq} f ON f.s = c.node AND f.p = {first} \
151 JOIN terms tt ON tt.id = ps.target JOIN terms pt ON pt.id = ps.path \
152 LEFT JOIN terms v ON v.id = f.o",
153 sh_in = sh("in"),
154 rest = named_node_id(rdf::REST.as_str()),
155 first = named_node_id(rdf::FIRST.as_str()),
156 );
157 vec![
158 Statement::new("DELETE FROM shapes_index"),
159 Statement::new("DELETE FROM shapes_in"),
160 Statement::new(index),
161 Statement::new(values),
162 ]
163}
164
165#[derive(Debug, Clone, Default, PartialEq)]
167pub struct PropertyShape {
168 pub datatype: Option<NamedNode>,
169 pub min: Option<i64>,
170 pub max: Option<i64>,
171 pub values_in: Vec<Term>,
172 pub pattern: Option<String>,
173 pub relationship: bool,
175}
176
177#[derive(Debug, Clone, Default, PartialEq)]
179pub struct ShapeIndex {
180 pub by_class: BTreeMap<NamedNode, BTreeMap<NamedNode, PropertyShape>>,
181}
182
183impl ShapeIndex {
184 pub fn is_empty(&self) -> bool {
185 self.by_class.is_empty()
186 }
187
188 pub fn get(&self, class: &NamedNode, path: &NamedNode) -> Option<&PropertyShape> {
190 self.by_class.get(class).and_then(|m| m.get(path))
191 }
192
193 pub fn load_request(caps: &Capabilities) -> Request {
195 let id = if caps.int64_as_text {
196 "CAST(id AS TEXT)"
197 } else {
198 "id"
199 };
200 Request::read(vec![
201 Statement::new(
202 "SELECT target, path, datatype, min_count, max_count, pattern, relationship FROM shapes_index",
203 ),
204 Statement::new(format!(
205 "SELECT target, path, {id}, lex, dt, lang, dir FROM shapes_in"
206 )),
207 ])
208 }
209
210 pub fn from_response(response: &Response) -> Result<Self> {
212 let mut me = Self::default();
213 let Some(index) = response.first() else {
214 return Ok(me);
215 };
216 for row in &index.rows {
217 let (Some(target), Some(path)) = (named(col(row, 0)?), named(col(row, 1)?)) else {
218 continue;
219 };
220 let shape = me
221 .by_class
222 .entry(target)
223 .or_default()
224 .entry(path)
225 .or_default();
226 shape.datatype = named(col(row, 2)?);
227 shape.min = col(row, 3)?.as_i64();
228 shape.max = col(row, 4)?.as_i64();
229 shape.pattern = col(row, 5)?.clone().into_string();
230 shape.relationship = col(row, 6)?.as_i64().unwrap_or(0) != 0;
231 }
232 let Some(values) = response.get(1) else {
233 return Ok(me);
234 };
235 for row in &values.rows {
236 let (Some(target), Some(path), Some(id)) = (
237 named(col(row, 0)?),
238 named(col(row, 1)?),
239 col(row, 2)?.as_i64(),
240 ) else {
241 continue;
242 };
243 let term = match col(row, 3)?.clone().into_string() {
244 Some(lex) => decode_row(
246 id,
247 lex,
248 col(row, 4)?.clone().into_string(),
249 col(row, 5)?.clone().into_string(),
250 col(row, 6)?.as_i64(),
251 )?,
252 None => match decode_inline(id) {
254 Some(t) => t,
255 None => continue,
256 },
257 };
258 let shape = me
259 .by_class
260 .entry(target)
261 .or_default()
262 .entry(path)
263 .or_default();
264 if !shape.values_in.contains(&term) {
265 shape.values_in.push(term);
266 }
267 }
268 Ok(me)
269 }
270}
271
272fn named(v: &crate::sql::SqlValue) -> Option<NamedNode> {
273 NamedNode::new(v.as_str()?).ok()
274}