Skip to main content

pyshacl_turbo_engine/
compile.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::expression::NodeExpression;
4use crate::functions::{Functions, NativeQuery};
5use bigdecimal::BigDecimal;
6use oxigraph::model::*;
7use spargebra::algebra::GraphPattern;
8use spargebra::{Query, SparqlParser};
9
10use crate::constraints::{self, Constraint};
11use crate::graph::*;
12use crate::path::Path;
13use crate::{Error, Result};
14use shacl_structural_model as model;
15
16#[cfg(test)]
17#[path = "compile_tests.rs"]
18mod tests;
19
20pub(crate) enum Target {
21    Node(Term),
22    Class(Term),
23    Subjects(NamedNode),
24    Objects(NamedNode),
25    Sparql {
26        query: Box<NativeQuery>,
27        bindings: Vec<(Variable, Term)>,
28    },
29}
30
31pub(crate) struct Rule {
32    pub node: Term,
33    pub order: BigDecimal,
34    pub deactivated: bool,
35    pub conditions: Vec<usize>,
36    pub queries: Vec<NativeQuery>,
37    pub triple: Option<[NodeExpression; 3]>,
38}
39
40pub(crate) struct Shape {
41    pub node: Term,
42    pub path: Option<Path>,
43    pub path_node: Option<Term>,
44    pub order: BigDecimal,
45    pub deactivated: bool,
46    pub targets: Vec<Target>,
47    pub constraints: Vec<Constraint>,
48    pub properties: Vec<usize>,
49    pub rules: Vec<Rule>,
50    pub messages: Vec<Term>,
51    pub severity: NamedNode,
52}
53
54/// Immutable compiled shapes and SPARQL queries; safe to share across validations.
55pub struct CompiledShapes {
56    pub(crate) graph: Dataset,
57    pub(crate) shapes: Vec<Shape>,
58    pub(crate) execution_order: Vec<usize>,
59    pub(crate) functions: Functions,
60    pub(crate) namespaces: Vec<(String, String)>,
61}
62
63fn order(value: Option<Term>, node: &Term) -> Result<BigDecimal> {
64    match value {
65        None => Ok(BigDecimal::from(0)),
66        Some(Term::Literal(v))
67            if ["integer", "decimal", "float", "double"]
68                .iter()
69                .any(|t| v.datatype() == iri(XSD, t).as_ref()) =>
70        {
71            v.value()
72                .parse()
73                .map_err(|_| Error::Invalid(format!("Invalid sh:order on {node}")))
74        }
75        _ => Err(Error::Invalid(format!(
76            "sh:order must be numeric on {node}"
77        ))),
78    }
79}
80
81fn query(
82    store: &Dataset,
83    declaration: model::SparqlQuery<Term>,
84    construct: bool,
85    namespaces: &[(String, String)],
86) -> Result<NativeQuery> {
87    let Term::Literal(text) = declaration.text else {
88        return Err(Error::Invalid(
89            "SPARQL query must be a string literal".into(),
90        ));
91    };
92    if text.datatype() != iri(XSD, "string").as_ref() {
93        return Err(Error::Invalid("SPARQL query must be an xsd:string".into()));
94    }
95    let mut parsed = SparqlParser::new()
96        .parse_query(&format!(
97            "{}\n{}",
98            crate::sparql::prefixes(store, declaration.prefixes, namespaces)?,
99            text.value()
100        ))
101        .map_err(|e| Error::Invalid(format!("SPARQL parse error: {e}")))?;
102    if (construct && !matches!(parsed, Query::Construct { .. }))
103        || (!construct && !matches!(parsed, Query::Select { .. }))
104    {
105        return Err(Error::Invalid(format!(
106            "SPARQL query must be {}",
107            if construct { "CONSTRUCT" } else { "SELECT" }
108        )));
109    }
110    match &mut parsed {
111        Query::Select { pattern, .. } => {
112            let mut has_this = false;
113            pattern.on_in_scope_variable(|v| has_this |= v.as_str() == "this");
114            if !has_this {
115                return Err(Error::Invalid("SPARQL target must project ?this".into()));
116            }
117        }
118        Query::Construct { pattern, .. } => {
119            // Oxigraph substitutions require a projected variable. Make the
120            // SHACL prebinding explicit in the algebra, even when $this occurs
121            // only in a filter/template or is absent from a constant rule.
122            let mut variables = HashSet::new();
123            pattern.on_in_scope_variable(|v| {
124                variables.insert(v.clone());
125            });
126            variables.insert(Variable::new_unchecked("this"));
127            *pattern = GraphPattern::Project {
128                inner: Box::new(pattern.clone()),
129                variables: variables.into_iter().collect(),
130            };
131        }
132        _ => unreachable!("Query kind was checked above"),
133    }
134    Ok(NativeQuery::new(parsed))
135}
136
137fn lower_target(
138    graph: &Dataset,
139    target: model::Target<Term>,
140    namespaces: &[(String, String)],
141) -> Result<Target> {
142    use crate::structural::named_iri;
143    Ok(match target {
144        model::Target::Node(node) => Target::Node(node),
145        model::Target::Class(class) => Target::Class(named_iri(class).into()),
146        model::Target::ImplicitClass(class) => Target::Class(class),
147        model::Target::SubjectsOf(predicate) => Target::Subjects(named_iri(predicate)),
148        model::Target::ObjectsOf(predicate) => Target::Objects(named_iri(predicate)),
149        model::Target::Sparql(target) => Target::Sparql {
150            query: Box::new(query(graph, target.select, false, namespaces)?),
151            bindings: Vec::new(),
152        },
153        model::Target::Custom(target) => {
154            let definition = crate::structural::target_type(graph, &target.target_type)?;
155            let mut bindings = Vec::new();
156            for parameter in crate::functions::lower_parameters(definition.parameters)? {
157                let values = &target
158                    .parameters
159                    .iter()
160                    .find(|p| p.path.as_str() == parameter.path.as_str())
161                    .ok_or_else(|| {
162                        Error::Invalid(format!("Missing target parameter {}", parameter.path))
163                    })?
164                    .values;
165                if values.len() > 1 || (values.is_empty() && !parameter.optional) {
166                    return Err(Error::Invalid(format!(
167                        "Target {} requires one value for {}",
168                        target.id, parameter.path
169                    )));
170                }
171                if let Some(value) = values.first() {
172                    bindings.push((parameter.variable, value.clone()));
173                }
174            }
175            let mut parsed = query(graph, definition.select, false, namespaces)?.parsed;
176            crate::functions::project_bindings(
177                &mut parsed,
178                &bindings.iter().map(|(v, _)| v.clone()).collect::<Vec<_>>(),
179            );
180            Target::Sparql {
181                query: Box::new(NativeQuery::new(parsed)),
182                bindings,
183            }
184        }
185    })
186}
187
188fn lower_rule(
189    graph: &Dataset,
190    declaration: model::ShaclRule<Term>,
191    indices: &HashMap<Term, usize>,
192    namespaces: &[(String, String)],
193) -> Result<Rule> {
194    let mut queries = Vec::new();
195    let mut triple = None;
196    for kind in declaration.kinds {
197        match kind {
198            model::RuleKind::Sparql(body) => queries.push(query(graph, body, true, namespaces)?),
199            model::RuleKind::Triple(body) => {
200                if triple.is_some() {
201                    return Err(Error::Invalid("Multiple TripleRule bodies".into()));
202                }
203                triple = Some([
204                    NodeExpression::lower(body.subject, indices)?,
205                    NodeExpression::lower(body.predicate, indices)?,
206                    NodeExpression::lower(body.object, indices)?,
207                ]);
208            }
209        }
210    }
211    Ok(Rule {
212        order: order(declaration.order, &declaration.id)?,
213        node: declaration.id,
214        deactivated: declaration.deactivated,
215        conditions: declaration
216            .conditions
217            .iter()
218            .map(|n| {
219                indices
220                    .get(n)
221                    .copied()
222                    .ok_or_else(|| Error::Invalid("Unknown condition shape".into()))
223            })
224            .collect::<Result<_>>()?,
225        queries,
226        triple,
227    })
228}
229
230fn check_vocabulary(
231    store: &Dataset,
232    advanced: bool,
233    executable_nodes: &HashSet<Term>,
234) -> Result<()> {
235    // Deliberately limited to supported features. This also catches
236    // misspelled SHACL terms instead of silently treating them as annotations.
237    const ALLOWED: &[&str] = &[
238        "targetNode",
239        "targetClass",
240        "targetSubjectsOf",
241        "targetObjectsOf",
242        "target",
243        "select",
244        "prefixes",
245        "property",
246        "path",
247        "minCount",
248        "maxCount",
249        "hasValue",
250        "message",
251        "severity",
252        "deactivated",
253        "order",
254        "rule",
255        "condition",
256        "construct",
257        "name",
258        "description",
259        "group",
260        "defaultValue",
261        "nodeKind",
262        "optional",
263        "parameter",
264        "validator",
265        "nodeValidator",
266        "propertyValidator",
267        "ask",
268        "labelTemplate",
269        "label",
270        "keyParameter",
271        "returnType",
272        "subject",
273        "predicate",
274        "object",
275        "expression",
276        "filterShape",
277        "nodes",
278        "union",
279        "intersection",
280    ];
281    for quad in store.iter() {
282        let quad = quad.into_owned();
283        // Function/component declarations have implicit global registration.
284        if advanced
285            && quad.predicate == iri(RDF, "type")
286            && let Term::NamedNode(ref n) = quad.object
287            && let Some(name) = n.as_str().strip_prefix(SH)
288            && ["Function", "SHACLFunction", "JSFunction"].contains(&name)
289        {
290            return Err(Error::Unsupported(format!("sh:{name}")));
291        }
292        // A shapes graph may also contain ordinary data and prior reports.
293        // Only executable resources determine the supported SHACL program.
294        if !executable_nodes.contains(&Term::from(quad.subject.clone())) {
295            continue;
296        }
297        if let Some(name) = quad.predicate.as_str().strip_prefix(SH) {
298            if !ALLOWED.contains(&name) && !constraints::PARAMETERS.contains(&name) {
299                return Err(Error::Unsupported(format!("sh:{name} on {}", quad.subject)));
300            }
301            if !advanced && ["target", "rule", "construct", "condition"].contains(&name) {
302                return Err(Error::Unsupported(format!(
303                    "sh:{name} requires advanced=True"
304                )));
305            }
306        }
307        if quad.predicate == iri(RDF, "type")
308            && let Term::NamedNode(ref n) = quad.object
309            && let Some(name) = n.as_str().strip_prefix(SH)
310            && ![
311                "NodeShape",
312                "PropertyShape",
313                "SPARQLRule",
314                "SPARQLTarget",
315                "SPARQLConstraint",
316                "ConstraintComponent",
317                "Parameter",
318                "SPARQLAskValidator",
319                "SPARQLSelectValidator",
320                "SPARQLFunction",
321                "TripleRule",
322                "SPARQLTargetType",
323                "TargetType",
324                "SPARQLConstraintObject",
325            ]
326            .contains(&name)
327        {
328            return Err(Error::Unsupported(format!("sh:{name}")));
329        }
330    }
331    Ok(())
332}
333
334impl CompiledShapes {
335    /// Shape identities discovered by the shared structural compiler.
336    pub fn shape_nodes(&self) -> impl Iterator<Item = &Term> {
337        self.shapes.iter().map(|shape| &shape.node)
338    }
339
340    pub fn compile(store: Dataset, advanced: bool) -> Result<Self> {
341        Self::compile_with_boolean_policy(store, advanced, true)
342    }
343
344    pub fn compile_with_boolean_policy(
345        store: Dataset,
346        advanced: bool,
347        numeric_booleans: bool,
348    ) -> Result<Self> {
349        Self::compile_with_namespaces(store, advanced, numeric_booleans, &[])
350    }
351
352    pub fn compile_with_namespaces(
353        mut store: Dataset,
354        advanced: bool,
355        numeric_booleans: bool,
356        namespaces: &[(String, String)],
357    ) -> Result<Self> {
358        let original = store.clone();
359        if !numeric_booleans {
360            let replaced: Vec<_> = store.iter().filter(|q| {
361                ["closed", "uniqueLang", "deactivated", "qualifiedValueShapesDisjoint", "optional"].iter().any(|p| q.predicate == iri(SH, p).as_ref())
362                    && matches!(q.object, TermRef::Literal(l) if l.datatype() == iri(XSD, "boolean").as_ref() && l.value() == "1")
363            }).map(|q| q.into_owned()).collect();
364            for mut quad in replaced {
365                store.remove(&quad);
366                quad.object = Literal::from(false).into();
367                store.insert(&quad);
368            }
369        }
370        if store.iter().any(|q| !q.graph_name.is_default_graph()) {
371            return Err(Error::Unsupported("named shapes graphs".into()));
372        }
373        // Validate all path descriptions, including standalone declarations,
374        // without treating their subjects as executable shapes.
375        let mut paths = HashMap::new();
376        for q in store.quads_for_pattern(None, Some(iri(SH, "path").as_ref()), None, None) {
377            let node = q.object.into_owned();
378            if let std::collections::hash_map::Entry::Vacant(entry) = paths.entry(node.clone()) {
379                entry.insert(Path::read(&store, &node)?);
380            }
381        }
382        let components = crate::components::Component::discover(&store)?;
383        let functions = if advanced {
384            Functions::compile(&store, namespaces)?
385        } else {
386            Functions::default()
387        };
388        let mut nodes = HashSet::new();
389        for kind in ["NodeShape", "PropertyShape"] {
390            nodes.extend(subjects(
391                &store,
392                &iri(RDF, "type"),
393                Some(&iri(SH, kind).into()),
394            )?);
395        }
396        for name in [
397            "targetNode",
398            "targetClass",
399            "targetSubjectsOf",
400            "targetObjectsOf",
401            "target",
402            "rule",
403        ] {
404            nodes.extend(subjects(&store, &iri(SH, name), None)?);
405        }
406        let mut parameters = instances(&store, &iri(SH, "Parameter").into())?;
407        parameters.extend(
408            store
409                .quads_for_pattern(None, Some(iri(SH, "parameter").as_ref()), None, None)
410                .map(|q| q.object.into_owned()),
411        );
412        for name in ["property", "node"] {
413            // Parameter declarations can describe their argument with sh:node
414            // or sh:property. This metadata is not a validation entry point.
415            // An explicit shape type, target or shape reference still gives a
416            // parameter a second, executable role.
417            nodes.extend(
418                subjects(&store, &iri(SH, name), None)?
419                    .into_iter()
420                    .filter(|n| !parameters.contains(n)),
421            );
422        }
423        nodes.extend(constraints::shape_references(&store)?);
424        for quad in store.quads_for_pattern(None, Some(iri(SH, "filterShape").as_ref()), None, None)
425        {
426            nodes.insert(quad.object.into_owned());
427        }
428        let mut executable_nodes = nodes.clone();
429        for node in &nodes {
430            for predicate in ["rule", "target", "sparql"] {
431                executable_nodes.extend(values(&store, node, predicate)?);
432            }
433        }
434        for component in &components {
435            executable_nodes.insert(component.node.clone().into());
436            executable_nodes.extend(
437                component
438                    .parameters
439                    .iter()
440                    .map(|p| p.declaration.id.clone()),
441            );
442            executable_nodes.extend(component.validators.iter().cloned());
443        }
444        // Shape roles come from types, targets and shape-expecting predicates.
445        // Constraint parameter annotations alone do not turn metadata into an
446        // executable shape (e.g. sh:path on a function/component parameter).
447        executable_nodes.extend(nodes.iter().cloned());
448        check_vocabulary(&store, advanced, &executable_nodes)?;
449        let mut nodes: Vec<_> = nodes.into_iter().collect();
450        nodes.sort_by_key(ToString::to_string);
451        let indices: HashMap<_, _> = nodes.iter().cloned().zip(0..).collect();
452        let mut shapes = Vec::new();
453        for node in nodes {
454            let declaration = crate::structural::read_shape(
455                &store,
456                &node,
457                &paths,
458                advanced,
459                &functions,
460                &components,
461            )?;
462            let (declaration, path, path_node) = match declaration {
463                crate::structural::ShapeDeclaration::Node(shape) => (*shape, None, None),
464                crate::structural::ShapeDeclaration::Property(shape) => (
465                    model::NodeShape {
466                        id: shape.id,
467                        targets: shape.targets,
468                        components: shape.components,
469                        property_shapes: shape.property_shapes,
470                        deactivated: shape.deactivated,
471                        severity: shape.severity,
472                        messages: shape.messages,
473                        rules: shape.rules,
474                        order: shape.order,
475                    },
476                    Some(Path::from(shape.path)),
477                    shape.path_node,
478                ),
479            };
480            let targets = declaration
481                .targets
482                .into_iter()
483                .map(|target| lower_target(&store, target, namespaces))
484                .collect::<Result<Vec<_>>>()?;
485            let mut constraints = Vec::new();
486            for component in declaration.components {
487                match component {
488                    model::Component::Sparql(sparql) => {
489                        constraints.push(Constraint::Sparql(Box::new(crate::sparql::lower(
490                            &store,
491                            *sparql,
492                            path.as_ref(),
493                            namespaces,
494                        )?)))
495                    }
496                    model::Component::Expression(expression) => {
497                        constraints.push(Constraint::NodeExpression {
498                            node: expression.expression.node.clone(),
499                            expression: NodeExpression::lower(expression.expression, &indices)?,
500                            messages: expression.messages,
501                        })
502                    }
503                    model::Component::Custom(application) => {
504                        let definition = components
505                            .iter()
506                            .find(|c| c.node.as_str() == application.component.as_str())
507                            .ok_or_else(|| {
508                                Error::Invalid(format!(
509                                    "Unknown constraint component {}",
510                                    application.component
511                                ))
512                            })?;
513                        constraints.extend(definition.lower(
514                            &store,
515                            application,
516                            path.as_ref(),
517                            namespaces,
518                        )?);
519                    }
520                    core => {
521                        if let Some(constraint) = constraints::lower_one(
522                            &store,
523                            &declaration.id,
524                            path.is_some(),
525                            &indices,
526                            core,
527                        )? {
528                            constraints.push(constraint);
529                        }
530                    }
531                }
532            }
533            let properties = declaration
534                .property_shapes
535                .iter()
536                .map(|n| {
537                    indices
538                        .get(n)
539                        .copied()
540                        .ok_or_else(|| Error::Invalid("Unknown property shape".into()))
541                })
542                .collect::<Result<_>>()?;
543            let mut rules = declaration
544                .rules
545                .into_iter()
546                .map(|rule| lower_rule(&store, rule, &indices, namespaces))
547                .collect::<Result<Vec<_>>>()?;
548            rules.sort_by(|a, b| {
549                a.order
550                    .cmp(&b.order)
551                    .then_with(|| a.node.to_string().cmp(&b.node.to_string()))
552            });
553            shapes.push(Shape {
554                order: order(declaration.order, &declaration.id)?,
555                deactivated: declaration.deactivated,
556                node: declaration.id,
557                path,
558                path_node,
559                targets,
560                constraints,
561                properties,
562                rules,
563                messages: declaration.messages,
564                severity: NamedNode::new_unchecked(declaration.severity.as_iri()),
565            });
566        }
567        for shape in &shapes {
568            for &p in &shape.properties {
569                if shapes[p].path.is_none() {
570                    return Err(Error::Invalid(format!(
571                        "sh:property points to {} without sh:path",
572                        shapes[p].node
573                    )));
574                }
575            }
576        }
577        let mut execution_order: Vec<_> = (0..shapes.len()).collect();
578        execution_order.sort_by(|&a, &b| shapes[a].order.cmp(&shapes[b].order));
579        Ok(Self {
580            graph: original,
581            shapes,
582            execution_order,
583            functions,
584            namespaces: namespaces.to_vec(),
585        })
586    }
587}