use crate::encoding::{decode_inline, decode_row, named_node_id, Tag, INT_OFFSET, PAYLOAD_BITS};
use crate::error::Result;
use crate::registry::{scoped_quads, SchemaRole};
use crate::sql::{col, Capabilities, Request, Response, Statement};
use oxrdf::vocab::rdf;
use oxrdf::{NamedNode, QuadRef, Term};
use spargebra::term::NamedNodePattern;
use spargebra::{GraphUpdateOperation, Update};
use std::collections::BTreeMap;
const SH: &str = "http://www.w3.org/ns/shacl#";
const SHAPE_LOCALS: [&str; 10] = [
"targetClass",
"property",
"path",
"datatype",
"minCount",
"maxCount",
"pattern",
"class",
"node",
"in",
];
fn sh(local: &str) -> i64 {
named_node_id(&format!("{SH}{local}"))
}
pub fn is_shape_quad(q: QuadRef<'_>) -> bool {
crate::registry::is_registry_quad(q) || is_shape_iri(q.predicate.as_str())
}
fn is_shape_iri(p: &str) -> bool {
p.strip_prefix(SH)
.is_some_and(|l| SHAPE_LOCALS.contains(&l))
}
pub fn update_touches_shapes(update: &Update) -> bool {
crate::registry::update_touches_registry(update) || touches_shape_triples(update)
}
fn touches_shape_triples(update: &Update) -> bool {
let pattern = |p: &NamedNodePattern| match p {
NamedNodePattern::Variable(_) => true,
NamedNodePattern::NamedNode(n) => is_shape_iri(n.as_str()),
};
update.operations.iter().any(|op| match op {
GraphUpdateOperation::InsertData { data } => {
data.iter().any(|q| is_shape_iri(q.predicate.as_str()))
}
GraphUpdateOperation::DeleteData { data } => {
data.iter().any(|q| is_shape_iri(q.predicate.as_str()))
}
GraphUpdateOperation::DeleteInsert { delete, insert, .. } => {
delete.iter().any(|q| pattern(&q.predicate))
|| insert.iter().any(|q| pattern(&q.predicate))
}
GraphUpdateOperation::Create { .. } => false,
GraphUpdateOperation::Load { .. }
| GraphUpdateOperation::Clear { .. }
| GraphUpdateOperation::Drop { .. } => true,
})
}
fn int_value(x: &str) -> String {
let int_base = Tag::Integer.base() + INT_OFFSET;
let k_int = Tag::Integer as i64;
format!(
"CASE WHEN (({x}) >> {PAYLOAD_BITS}) = {k_int} THEN ({x}) - {int_base} \
ELSE (SELECT CAST(n.num AS INTEGER) FROM terms n WHERE n.id = ({x}) AND n.nt IS NOT NULL) END"
)
}
pub fn refresh_statements() -> Vec<Statement> {
let sq = scoped_quads(SchemaRole::Shacl);
let ps = format!(
"SELECT t.o AS target, pr.o AS pshape, pa.o AS path \
FROM {sq} t JOIN {sq} pr ON pr.s = t.s AND pr.p = {property} \
JOIN {sq} pa ON pa.s = pr.o AND pa.p = {path} WHERE t.p = {target_class}",
property = sh("property"),
path = sh("path"),
target_class = sh("targetClass"),
);
let lex_of = |local: &str| {
format!(
"(SELECT v.lex FROM {sq} x JOIN terms v ON v.id = x.o WHERE x.s = ps.pshape AND x.p = {})",
sh(local)
)
};
let int_of = |local: &str| {
format!(
"(SELECT {} FROM {sq} x WHERE x.s = ps.pshape AND x.p = {})",
int_value("x.o"),
sh(local)
)
};
let rel = format!(
"(SELECT EXISTS (SELECT 1 FROM {sq} x WHERE x.s = ps.pshape AND x.p IN ({}, {})))",
sh("class"),
sh("node")
);
let index = format!(
"INSERT OR REPLACE INTO shapes_index(target, path, datatype, min_count, max_count, pattern, relationship) \
SELECT tt.lex, pt.lex, MAX({datatype}), MAX({min}), MAX({max}), MAX({pattern}), MAX({rel}) \
FROM ({ps}) ps JOIN terms tt ON tt.id = ps.target JOIN terms pt ON pt.id = ps.path \
GROUP BY tt.lex, pt.lex",
datatype = lex_of("datatype"),
min = int_of("minCount"),
max = int_of("maxCount"),
pattern = lex_of("pattern"),
);
let values = format!(
"WITH RECURSIVE cells(pshape, node) AS (\
SELECT x.s, x.o FROM {sq} x WHERE x.p = {sh_in} \
UNION SELECT c.pshape, r.o FROM cells c JOIN {sq} r ON r.s = c.node AND r.p = {rest}) \
INSERT OR REPLACE INTO shapes_in(target, path, id, lex, dt, lang, dir) \
SELECT tt.lex, pt.lex, f.o, v.lex, v.dt, v.lang, v.dir \
FROM ({ps}) ps JOIN cells c ON c.pshape = ps.pshape \
JOIN {sq} f ON f.s = c.node AND f.p = {first} \
JOIN terms tt ON tt.id = ps.target JOIN terms pt ON pt.id = ps.path \
LEFT JOIN terms v ON v.id = f.o",
sh_in = sh("in"),
rest = named_node_id(rdf::REST.as_str()),
first = named_node_id(rdf::FIRST.as_str()),
);
vec![
Statement::new("DELETE FROM shapes_index"),
Statement::new("DELETE FROM shapes_in"),
Statement::new(index),
Statement::new(values),
]
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PropertyShape {
pub datatype: Option<NamedNode>,
pub min: Option<i64>,
pub max: Option<i64>,
pub values_in: Vec<Term>,
pub pattern: Option<String>,
pub relationship: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ShapeIndex {
pub by_class: BTreeMap<NamedNode, BTreeMap<NamedNode, PropertyShape>>,
}
impl ShapeIndex {
pub fn is_empty(&self) -> bool {
self.by_class.is_empty()
}
pub fn get(&self, class: &NamedNode, path: &NamedNode) -> Option<&PropertyShape> {
self.by_class.get(class).and_then(|m| m.get(path))
}
pub fn load_request(caps: &Capabilities) -> Request {
let id = if caps.int64_as_text {
"CAST(id AS TEXT)"
} else {
"id"
};
Request::read(vec![
Statement::new(
"SELECT target, path, datatype, min_count, max_count, pattern, relationship FROM shapes_index",
),
Statement::new(format!(
"SELECT target, path, {id}, lex, dt, lang, dir FROM shapes_in"
)),
])
}
pub fn from_response(response: &Response) -> Result<Self> {
let mut me = Self::default();
let Some(index) = response.first() else {
return Ok(me);
};
for row in &index.rows {
let (Some(target), Some(path)) = (named(col(row, 0)?), named(col(row, 1)?)) else {
continue;
};
let shape = me
.by_class
.entry(target)
.or_default()
.entry(path)
.or_default();
shape.datatype = named(col(row, 2)?);
shape.min = col(row, 3)?.as_i64();
shape.max = col(row, 4)?.as_i64();
shape.pattern = col(row, 5)?.clone().into_string();
shape.relationship = col(row, 6)?.as_i64().unwrap_or(0) != 0;
}
let Some(values) = response.get(1) else {
return Ok(me);
};
for row in &values.rows {
let (Some(target), Some(path), Some(id)) = (
named(col(row, 0)?),
named(col(row, 1)?),
col(row, 2)?.as_i64(),
) else {
continue;
};
let term = match col(row, 3)?.clone().into_string() {
Some(lex) => decode_row(
id,
lex,
col(row, 4)?.clone().into_string(),
col(row, 5)?.clone().into_string(),
col(row, 6)?.as_i64(),
)?,
None => match decode_inline(id) {
Some(t) => t,
None => continue,
},
};
let shape = me
.by_class
.entry(target)
.or_default()
.entry(path)
.or_default();
if !shape.values_in.contains(&term) {
shape.values_in.push(term);
}
}
Ok(me)
}
}
fn named(v: &crate::sql::SqlValue) -> Option<NamedNode> {
NamedNode::new(v.as_str()?).ok()
}