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