Skip to main content

pyshacl_turbo_engine/
execute.rs

1use std::collections::{HashMap, HashSet};
2
3use oxigraph::model::*;
4use oxigraph::sparql::QueryResults;
5
6use crate::compile::{CompiledShapes, Shape, Target};
7use crate::constraints::Constraint;
8use crate::functions::Functions;
9use crate::graph::*;
10use crate::path::Path;
11use crate::{Error, Result};
12
13#[cfg(test)]
14#[path = "execute_tests.rs"]
15mod coverage_tests;
16
17#[derive(Clone, Debug)]
18pub struct Options {
19    pub iterate_rules: bool,
20    pub max_rule_iterations: usize,
21    pub max_validation_depth: usize,
22    pub allow_infos: bool,
23    pub allow_warnings: bool,
24    pub abort_on_first: bool,
25    pub focus_nodes: Vec<Term>,
26    pub use_shapes: Vec<Term>,
27    pub namespaces: Vec<(String, String)>,
28}
29
30impl Default for Options {
31    fn default() -> Self {
32        Self {
33            iterate_rules: false,
34            max_rule_iterations: 100,
35            max_validation_depth: 15,
36            allow_infos: false,
37            allow_warnings: false,
38            abort_on_first: false,
39            focus_nodes: Vec::new(),
40            use_shapes: Vec::new(),
41            namespaces: Vec::new(),
42        }
43    }
44}
45
46pub struct ValidationReport {
47    pub conforms: bool,
48    pub graph: Dataset,
49    pub text: String,
50    pub recursion_warnings: Vec<String>,
51}
52
53pub struct RuleExpansion {
54    pub graph: Dataset,
55    pub recursion_warnings: Vec<String>,
56}
57
58#[derive(Clone, Copy)]
59struct EvaluationStep {
60    shape: usize,
61    component: &'static str,
62}
63
64struct Evaluation {
65    path_values: HashMap<(Path, Term), Vec<Term>>,
66    memberships: HashMap<(Term, Term), bool>,
67    class_targets: HashMap<Term, HashSet<Term>>,
68    cached_terms: usize,
69    functions: Functions,
70    path: Vec<EvaluationStep>,
71    max_depth: usize,
72    warnings: Vec<String>,
73    batch: Option<EvaluationBatch>,
74}
75
76#[derive(Clone, Copy)]
77struct EvaluationBatch {
78    shape: usize,
79    depth: usize,
80    has_values: bool,
81}
82
83impl Evaluation {
84    fn new(max_depth: usize) -> Self {
85        Self {
86            path_values: HashMap::new(),
87            memberships: HashMap::new(),
88            class_targets: HashMap::new(),
89            cached_terms: 0,
90            functions: Functions::default(),
91            path: Vec::new(),
92            max_depth,
93            warnings: Vec::new(),
94            batch: None,
95        }
96    }
97
98    fn clear_traversals(&mut self) {
99        self.cached_terms = 0;
100        self.path_values.clear();
101        self.memberships.clear();
102        self.class_targets.clear();
103    }
104
105    fn reserve_traversal(&mut self, terms: usize) -> bool {
106        if terms > 32768 {
107            return false;
108        }
109        if self.path_values.len() + self.memberships.len() + self.class_targets.len() >= 1024
110            || self.cached_terms + terms > 32768
111        {
112            self.clear_traversals();
113        }
114        self.cached_terms += terms;
115        true
116    }
117
118    fn values(&mut self, path: &Path, data: &Dataset, focus: &Term) -> Result<Vec<Term>> {
119        if matches!(path, Path::Predicate(_)) {
120            return path.values(data, focus);
121        }
122        let key = (path.clone(), focus.clone());
123        if let Some(values) = self.path_values.get(&key) {
124            return Ok(values.clone());
125        }
126        let values = path.values(data, focus)?;
127        if self.reserve_traversal(values.len()) {
128            self.path_values.insert(key, values.clone());
129        }
130        Ok(values)
131    }
132
133    fn is_instance(&mut self, data: &Dataset, value: &Term, class: &Term) -> Result<bool> {
134        let key = (value.clone(), class.clone());
135        if let Some(result) = self.memberships.get(&key) {
136            return Ok(*result);
137        }
138        let result = is_instance(data, value, class)?;
139        if self.reserve_traversal(1) {
140            self.memberships.insert(key, result);
141        }
142        Ok(result)
143    }
144
145    fn targets(&mut self, data: &Dataset, class: &Term) -> Result<HashSet<Term>> {
146        if let Some(nodes) = self.class_targets.get(class) {
147            return Ok(nodes.clone());
148        }
149        let nodes = instances(data, class)?;
150        if self.reserve_traversal(nodes.len()) {
151            self.class_targets.insert(class.clone(), nodes.clone());
152        }
153        Ok(nodes)
154    }
155
156    fn describe_path(&self, shapes: &[Shape], current: Option<EvaluationStep>) -> String {
157        self.path
158            .iter()
159            .copied()
160            .chain(current)
161            .map(|step| format!("{} -> {}", shapes[step.shape].node, step.component))
162            .collect::<Vec<_>>()
163            .join(" -> ")
164    }
165
166    /// Match pySHACL's recursion_triggers: shape/constraint edges, not RDF nodes.
167    fn skip_recursive(
168        &mut self,
169        shape: usize,
170        component: &'static str,
171        target: usize,
172        shapes: &[Shape],
173    ) -> bool {
174        let alternating = self.path.last().is_some_and(|previous| {
175            matches!(
176                (previous.component, component),
177                ("NodeConstraintComponent", "PropertyConstraintComponent")
178                    | ("PropertyConstraintComponent", "NodeConstraintComponent")
179            )
180        });
181        let depth = self.path.len() + 1;
182        if depth < if alternating { 6 } else { 3 } {
183            return false;
184        }
185        let recursive = self.path.iter().enumerate().any(|(i, step)| {
186            step.shape == shape
187                && step.component == component
188                && self.path.get(i + 1).map_or(shape, |next| next.shape) == target
189        });
190        if recursive {
191            let path = self.describe_path(shapes, Some(EvaluationStep { shape, component }));
192            self.warnings.push(format!(
193                "Warning, A Recursive Shape was detected executing a recursive validation sequence {} levels deep. Backing out.\n{path}\nFor reference, see https://www.w3.org/TR/shacl/#shapes-recursion",
194                depth * 2
195            ));
196        }
197        recursive
198    }
199}
200
201struct Violation {
202    shape: usize,
203    focus: Term,
204    component: NamedNode,
205    message: String,
206    value: Option<Term>,
207    path: Option<Term>,
208    details: Vec<Violation>,
209    source_constraint: Option<Term>,
210    messages: Option<Vec<Term>>,
211}
212
213impl CompiledShapes {
214    /// Validate a consistent Store snapshot without RDF text conversion.
215    /// Rule additions commit in one Store transaction only after success.
216    pub fn validate_store(
217        &self,
218        store: &oxigraph::store::Store,
219        options: Options,
220        inplace: bool,
221    ) -> Result<ValidationReport> {
222        let before = store_snapshot(store)?;
223        let mut working = before.clone();
224        let report = self.validate(&mut working, options)?;
225        if inplace {
226            commit_store(store, &before, &working)?;
227        }
228        Ok(report)
229    }
230
231    /// Expand rules over a Store snapshot; optionally commit the additions.
232    pub fn expand_store(
233        &self,
234        store: &oxigraph::store::Store,
235        options: Options,
236        inplace: bool,
237    ) -> Result<RuleExpansion> {
238        let before = store_snapshot(store)?;
239        let mut working = before.clone();
240        let recursion_warnings = self.expand_rules(&mut working, options)?;
241        if inplace {
242            commit_store(store, &before, &working)?;
243        }
244        Ok(RuleExpansion {
245            graph: working,
246            recursion_warnings,
247        })
248    }
249
250    /// Inspect targets using the same evaluator as validation and rules.
251    /// The caller supplies the default-union view, reusable across calls.
252    pub fn shape_focus_nodes(
253        &self,
254        node: &str,
255        data: &Dataset,
256        advanced: bool,
257    ) -> Result<Vec<Term>> {
258        let shape = self
259            .shapes
260            .iter()
261            .find(|shape| shape.node.to_string() == node)
262            .ok_or_else(|| Error::Invalid(format!("Unknown shape: {node}")))?;
263        self.focus_nodes_with_advanced(shape, data, advanced, &mut Evaluation::new(15))
264    }
265
266    fn focus_nodes(
267        &self,
268        shape: &Shape,
269        data: &Dataset,
270        evaluation: &mut Evaluation,
271    ) -> Result<Vec<Term>> {
272        self.focus_nodes_with_advanced(shape, data, true, evaluation)
273    }
274
275    /// Reverse declared targets for active node shapes. Data is a reusable
276    /// default-union view. Skipped shapes are never evaluated.
277    pub fn targeting_index(
278        &self,
279        data: &Dataset,
280        skip: &HashSet<Term>,
281    ) -> Result<HashMap<Term, Vec<Term>>> {
282        let mut evaluation = Evaluation::new(15);
283        let mut index: HashMap<Term, Vec<Term>> = HashMap::new();
284        for shape in &self.shapes {
285            if shape.deactivated || shape.path.is_some() || skip.contains(&shape.node) {
286                continue;
287            }
288            for node in self.focus_nodes(shape, data, &mut evaluation)? {
289                index.entry(node).or_default().push(shape.node.clone());
290            }
291        }
292        for shapes in index.values_mut() {
293            shapes.sort_by_key(ToString::to_string);
294        }
295        Ok(index)
296    }
297
298    fn focus_nodes_with_advanced(
299        &self,
300        shape: &Shape,
301        data: &Dataset,
302        advanced: bool,
303        evaluation: &mut Evaluation,
304    ) -> Result<Vec<Term>> {
305        let mut nodes = HashSet::new();
306        for target in &shape.targets {
307            match target {
308                Target::Node(node) => {
309                    nodes.insert(node.clone());
310                }
311                Target::Class(class) => {
312                    nodes.extend(evaluation.targets(data, class)?);
313                }
314                Target::Subjects(predicate) => {
315                    nodes.extend(subjects(data, predicate, None)?);
316                }
317                Target::Objects(predicate) => {
318                    for quad in data.quads_for_pattern(
319                        None,
320                        Some(predicate.as_ref()),
321                        None,
322                        Some(GraphNameRef::DefaultGraph),
323                    ) {
324                        nodes.insert(quad.object.into_owned());
325                    }
326                }
327                Target::Sparql { query, bindings } => {
328                    if !advanced {
329                        continue;
330                    }
331                    let functions = self.functions.for_dataset(data);
332                    let mut evaluation = query.prepare(&functions, data).on_queryable_dataset(data);
333                    for (variable, value) in bindings {
334                        evaluation =
335                            evaluation.substitute_variable(variable.clone(), value.clone());
336                    }
337                    let QueryResults::Solutions(solutions) = evaluation.execute()? else {
338                        return Err(Error::Invalid(
339                            "SPARQL target did not return SELECT results".into(),
340                        ));
341                    };
342                    if !solutions.variables().iter().any(|v| v.as_str() == "this") {
343                        return Err(Error::Invalid("SPARQL target must project ?this".into()));
344                    }
345                    for solution in solutions {
346                        let solution = solution?;
347                        let node = solution.get("this").ok_or_else(|| {
348                            Error::Invalid("SPARQL target returned an unbound ?this".into())
349                        })?;
350                        nodes.insert(node.clone());
351                    }
352                }
353            }
354        }
355        let mut nodes: Vec<_> = nodes.into_iter().collect();
356        nodes.sort_by_key(ToString::to_string);
357        Ok(nodes)
358    }
359
360    #[allow(clippy::too_many_arguments)]
361    fn evaluate_child(
362        &self,
363        parent: usize,
364        component: &'static str,
365        index: usize,
366        focus: &Term,
367        data: &Dataset,
368        results: &mut Vec<Violation>,
369        evaluation: &mut Evaluation,
370    ) -> Result<()> {
371        evaluation.path.push(EvaluationStep {
372            shape: parent,
373            component,
374        });
375        let result = self.evaluate_batch(
376            index,
377            std::slice::from_ref(focus),
378            data,
379            results,
380            evaluation,
381        );
382        evaluation.path.pop();
383        result
384    }
385
386    fn evaluate_batch(
387        &self,
388        index: usize,
389        focus: &[Term],
390        data: &Dataset,
391        results: &mut Vec<Violation>,
392        evaluation: &mut Evaluation,
393    ) -> Result<()> {
394        let shape = &self.shapes[index];
395        let batch = if focus.len() > 1
396            && !shape.deactivated
397            && shape
398                .constraints
399                .iter()
400                .any(|constraint| matches!(constraint, Constraint::Qualified { .. }))
401        {
402            // pySHACL's qualified-count empty-input shortcut applies to the
403            // whole focus batch. Nested property validations receive one
404            // focus each, so their scopes must not inherit this batch.
405            let mut has_values = shape.path.is_none();
406            if let Some(path) = &shape.path {
407                for node in focus {
408                    if !evaluation.values(path, data, node)?.is_empty() {
409                        has_values = true;
410                        break;
411                    }
412                }
413            }
414            Some(EvaluationBatch {
415                shape: index,
416                depth: evaluation.path.len(),
417                has_values,
418            })
419        } else {
420            None
421        };
422        let previous = std::mem::replace(&mut evaluation.batch, batch);
423        let result = focus
424            .iter()
425            .try_for_each(|node| self.evaluate_inner(index, node, data, results, evaluation));
426        evaluation.batch = previous;
427        result
428    }
429
430    fn evaluate_inner(
431        &self,
432        index: usize,
433        focus: &Term,
434        data: &Dataset,
435        results: &mut Vec<Violation>,
436        evaluation: &mut Evaluation,
437    ) -> Result<()> {
438        let functions = evaluation.functions.clone();
439        let shape = &self.shapes[index];
440        if shape.deactivated {
441            return Ok(());
442        }
443        if evaluation.path.len() >= evaluation.max_depth {
444            return Err(Error::Runtime(format!(
445                "Validation path too deep!\n{}",
446                evaluation.describe_path(&self.shapes, None)
447            )));
448        }
449        let values = if let Some(path) = &shape.path {
450            evaluation.values(path, data, focus)?
451        } else {
452            vec![focus.clone()]
453        };
454        let violation = |component, value: Option<&Term>| Violation {
455            shape: index,
456            focus: focus.clone(),
457            component: iri(SH, component),
458            message: String::new(),
459            value: value.cloned(),
460            path: shape.path_node.clone(),
461            details: Vec::new(),
462            source_constraint: None,
463            messages: None,
464        };
465        for constraint in &shape.constraints {
466            match constraint {
467                Constraint::NodeExpression {
468                    expression,
469                    node,
470                    messages,
471                } => {
472                    for value in &values {
473                        let nodes = expression.evaluate(value, data, &functions, &mut |i, n| {
474                            let mut nested = Vec::new();
475                            self.evaluate_child(
476                                index,
477                                "ExpressionConstraintComponent",
478                                i,
479                                n,
480                                data,
481                                &mut nested,
482                                evaluation,
483                            )?;
484                            Ok(nested.is_empty())
485                        })?;
486                        if nodes != HashSet::from([Literal::from(true).into()]) {
487                            let mut result =
488                                violation("ExpressionConstraintComponent", Some(value));
489                            result.source_constraint = Some(node.clone());
490                            if !messages.is_empty() {
491                                result.messages = Some(messages.clone());
492                            }
493                            results.push(result);
494                        }
495                    }
496                }
497                Constraint::Sparql(query) => {
498                    let inputs = if query.per_value {
499                        values.iter().map(Some).collect()
500                    } else {
501                        vec![None]
502                    };
503                    for input in inputs {
504                        for row in query.evaluate(
505                            data,
506                            &self.graph,
507                            &shape.node,
508                            focus,
509                            &shape.messages,
510                            input,
511                            &functions,
512                        )? {
513                            let mut result =
514                                violation("SPARQLConstraintComponent", row.value.as_ref());
515                            result.focus = row.focus;
516                            if result.value.is_none() && shape.path.is_none() {
517                                result.value = Some(focus.clone());
518                            }
519                            if query.per_value && shape.path.is_some() {
520                                // Match pySHACL's optional sh:value omission for
521                                // ASK validators attached to property shapes.
522                                result.value = None;
523                            }
524                            if row.path.is_some() {
525                                result.path = row.path;
526                            }
527                            if let Some(component) = &query.component {
528                                result.component = component.clone();
529                            } else {
530                                result.source_constraint = Some(query.node.clone());
531                            }
532                            if !row.messages.is_empty() {
533                                result.messages = Some(row.messages);
534                            }
535                            results.push(result);
536                        }
537                    }
538                }
539                Constraint::Count { minimum, count } => {
540                    if !count.allows(values.len(), *minimum) {
541                        results.push(violation(
542                            if *minimum {
543                                "MinCountConstraintComponent"
544                            } else {
545                                "MaxCountConstraintComponent"
546                            },
547                            None,
548                        ));
549                    }
550                }
551                Constraint::HasValue(required) => {
552                    if !values.contains(required) {
553                        results.push(violation("HasValueConstraintComponent", None));
554                    }
555                }
556                Constraint::Class(class) => {
557                    for value in &values {
558                        if !evaluation.is_instance(data, value, class)? {
559                            results.push(violation("ClassConstraintComponent", Some(value)));
560                        }
561                    }
562                }
563                Constraint::Datatype(datatype) => {
564                    for value in &values {
565                        if !crate::value::datatype(value, datatype) {
566                            results.push(violation("DatatypeConstraintComponent", Some(value)));
567                        }
568                    }
569                }
570                Constraint::NodeKind(kind) => {
571                    for value in &values {
572                        let valid = match value {
573                            Term::NamedNode(_) => {
574                                ["IRI", "BlankNodeOrIRI", "IRIOrLiteral"].contains(&kind.as_str())
575                            }
576                            Term::BlankNode(_) => {
577                                ["BlankNode", "BlankNodeOrIRI", "BlankNodeOrLiteral"]
578                                    .contains(&kind.as_str())
579                            }
580                            Term::Literal(_) => ["Literal", "BlankNodeOrLiteral", "IRIOrLiteral"]
581                                .contains(&kind.as_str()),
582                        };
583                        if !valid {
584                            results.push(violation("NodeKindConstraintComponent", Some(value)));
585                        }
586                    }
587                }
588                Constraint::Pattern(regex) => {
589                    for value in &values {
590                        let valid = crate::value::lexical(value)
591                            .map(|text| regex.is_match(text))
592                            .transpose()?
593                            .unwrap_or(false);
594                        if !valid {
595                            results.push(violation("PatternConstraintComponent", Some(value)));
596                        }
597                    }
598                }
599                Constraint::Expression { component, query } => {
600                    for value in &values {
601                        if !crate::value::expression(query, data, value, None)? {
602                            results.push(violation(component, Some(value)));
603                        }
604                    }
605                }
606                Constraint::Length { minimum, count } => {
607                    for value in &values {
608                        let valid = (*minimum && count.is_zero())
609                            || crate::value::lexical(value)
610                                .is_some_and(|s| count.allows(s.chars().count(), *minimum));
611                        if !valid {
612                            results.push(violation(
613                                if *minimum {
614                                    "MinLengthConstraintComponent"
615                                } else {
616                                    "MaxLengthConstraintComponent"
617                                },
618                                Some(value),
619                            ));
620                        }
621                    }
622                }
623                Constraint::LanguageIn(ranges) => {
624                    for value in &values {
625                        let valid = if let Term::Literal(l) = value {
626                            l.language().is_some_and(|lang| {
627                                ranges.iter().any(|r| {
628                                    r == "*"
629                                        || lang == r
630                                        || lang
631                                            .strip_prefix(r)
632                                            .is_some_and(|suffix| suffix.starts_with('-'))
633                                })
634                            })
635                        } else {
636                            false
637                        };
638                        if !valid {
639                            results.push(violation("LanguageInConstraintComponent", Some(value)));
640                        }
641                    }
642                }
643                Constraint::UniqueLang => {
644                    let mut seen = HashSet::new();
645                    let mut repeated = HashSet::new();
646                    for value in &values {
647                        if let Term::Literal(l) = value
648                            && let Some(lang) = l.language()
649                            && !seen.insert(lang)
650                        {
651                            repeated.insert(lang);
652                        }
653                    }
654                    for _ in repeated {
655                        results.push(violation("UniqueLangConstraintComponent", None));
656                    }
657                }
658                Constraint::In(allowed) => {
659                    for value in &values {
660                        if !allowed.contains(value) {
661                            results.push(violation("InConstraintComponent", Some(value)));
662                        }
663                    }
664                }
665                Constraint::Pair {
666                    kind,
667                    predicate,
668                    query,
669                } => {
670                    let others = objects(data, focus, predicate)?;
671                    if let Some(query) = query {
672                        for value in &values {
673                            for other in &others {
674                                if !crate::value::expression(query, data, value, Some(other))? {
675                                    results.push(violation(kind, Some(value)));
676                                }
677                            }
678                        }
679                    } else if *kind == "EqualsConstraintComponent" {
680                        for value in values
681                            .iter()
682                            .filter(|v| !others.contains(v))
683                            .chain(others.iter().filter(|v| !values.contains(v)))
684                        {
685                            results.push(violation(kind, Some(value)));
686                        }
687                    } else {
688                        for value in values.iter().filter(|v| others.contains(v)) {
689                            results.push(violation(kind, Some(value)));
690                        }
691                    }
692                }
693                Constraint::Node(target) => {
694                    if !values.is_empty()
695                        && evaluation.skip_recursive(
696                            index,
697                            "NodeConstraintComponent",
698                            *target,
699                            &self.shapes,
700                        )
701                    {
702                        continue;
703                    }
704                    for value in &values {
705                        let mut details = Vec::new();
706                        self.evaluate_child(
707                            index,
708                            "NodeConstraintComponent",
709                            *target,
710                            value,
711                            data,
712                            &mut details,
713                            evaluation,
714                        )?;
715                        if !details.is_empty() {
716                            let mut parent = violation("NodeConstraintComponent", Some(value));
717                            parent.details = details;
718                            results.push(parent);
719                        }
720                    }
721                }
722                Constraint::Logical { kind, shapes } => {
723                    // Only sh:not has the reference warn-and-skip policy.
724                    // and/or/xone recursion is bounded by max_validation_depth.
725                    if *kind == "NotConstraintComponent"
726                        && shapes.iter().any(|&target| {
727                            evaluation.skip_recursive(index, kind, target, &self.shapes)
728                        })
729                    {
730                        continue;
731                    }
732                    for value in &values {
733                        let mut count = 0;
734                        for &target in shapes {
735                            let mut nested = Vec::new();
736                            self.evaluate_child(
737                                index,
738                                kind,
739                                target,
740                                value,
741                                data,
742                                &mut nested,
743                                evaluation,
744                            )?;
745                            count += usize::from(nested.is_empty());
746                        }
747                        let valid = match *kind {
748                            "NotConstraintComponent" => count == 0,
749                            "AndConstraintComponent" => count == shapes.len(),
750                            "OrConstraintComponent" => count > 0,
751                            "XoneConstraintComponent" => count == 1,
752                            _ => unreachable!(),
753                        };
754                        if !valid {
755                            results.push(violation(kind, Some(value)));
756                        }
757                    }
758                }
759                Constraint::Qualified {
760                    shape: target,
761                    minimum,
762                    maximum,
763                    disjoint,
764                    siblings,
765                } => {
766                    let has_values = evaluation
767                        .batch
768                        .filter(|batch| {
769                            batch.shape == index && batch.depth == evaluation.path.len()
770                        })
771                        .map_or(!values.is_empty(), |batch| batch.has_values);
772                    if !has_values
773                        && !disjoint
774                        && minimum.as_ref().is_none_or(|min| min.allows(0, true))
775                    {
776                        continue;
777                    }
778                    if evaluation.skip_recursive(
779                        index,
780                        "QualifiedValueShapeConstraintComponent",
781                        *target,
782                        &self.shapes,
783                    ) {
784                        continue;
785                    }
786                    let mut count = 0usize;
787                    for value in &values {
788                        let mut nested = Vec::new();
789                        self.evaluate_child(
790                            index,
791                            "QualifiedValueShapeConstraintComponent",
792                            *target,
793                            value,
794                            data,
795                            &mut nested,
796                            evaluation,
797                        )?;
798                        if !nested.is_empty() {
799                            continue;
800                        }
801                        let mut disjoint = true;
802                        for &sibling in siblings {
803                            let mut sibling_results = Vec::new();
804                            self.evaluate_child(
805                                index,
806                                "QualifiedValueShapeConstraintComponent",
807                                sibling,
808                                value,
809                                data,
810                                &mut sibling_results,
811                                evaluation,
812                            )?;
813                            if sibling_results.is_empty() {
814                                disjoint = false;
815                                break;
816                            }
817                        }
818                        count += usize::from(disjoint);
819                    }
820                    if minimum.as_ref().is_some_and(|min| !min.allows(count, true)) {
821                        results.push(violation("QualifiedMinCountConstraintComponent", None));
822                    }
823                    if maximum
824                        .as_ref()
825                        .is_some_and(|max| !max.allows(count, false))
826                    {
827                        results.push(violation("QualifiedMaxCountConstraintComponent", None));
828                    }
829                }
830                Constraint::Closed(allowed) => {
831                    for value in &values {
832                        let Ok(node) = subject(value) else {
833                            continue;
834                        };
835                        for quad in data.quads_for_pattern(
836                            Some(node.as_ref()),
837                            None,
838                            None,
839                            Some(GraphNameRef::DefaultGraph),
840                        ) {
841                            let quad = quad.into_owned();
842                            if quad.predicate == iri(RDF, "type")
843                                && quad.object == Term::from(iri(RDFS, "Resource"))
844                            {
845                                continue;
846                            }
847                            if !allowed.contains(&quad.predicate) {
848                                let mut result =
849                                    violation("ClosedConstraintComponent", Some(&quad.object));
850                                result.path = Some(quad.predicate.into());
851                                results.push(result);
852                            }
853                        }
854                    }
855                }
856            }
857        }
858        for &property in &shape.properties {
859            if !values.is_empty()
860                && evaluation.skip_recursive(
861                    index,
862                    "PropertyConstraintComponent",
863                    property,
864                    &self.shapes,
865                )
866            {
867                continue;
868            }
869            for value in &values {
870                self.evaluate_child(
871                    index,
872                    "PropertyConstraintComponent",
873                    property,
874                    value,
875                    data,
876                    results,
877                    evaluation,
878                )?;
879            }
880        }
881        Ok(())
882    }
883
884    /// Mutates only this explicitly supplied store. Python validates a fresh store
885    /// and commits additions to the caller's Graph only after successful execution.
886    /// Whether validation needs a mutable working dataset for selected SHACL rules.
887    pub fn has_active_rules(&self, options: &Options) -> bool {
888        self.execution_order.iter().any(|&index| {
889            let shape = &self.shapes[index];
890            (options.use_shapes.is_empty() || options.use_shapes.contains(&shape.node))
891                && shape.rules.iter().any(|rule| !rule.deactivated)
892        })
893    }
894
895    pub fn expand_rules(&self, data: &mut Dataset, options: Options) -> Result<Vec<String>> {
896        if self.has_active_rules(&options) && data.iter().any(|q| !q.graph_name.is_default_graph())
897        {
898            return Ok(self.expand_rules_delta(data, options)?.recursion_warnings);
899        }
900        self.expand_rules_inner(data, options, None)
901    }
902
903    /// Expand rules while retaining only successfully inserted statements.
904    pub fn expand_rules_delta(
905        &self,
906        data: &mut Dataset,
907        options: Options,
908    ) -> Result<RuleExpansion> {
909        let mut additions = Dataset::new();
910        let recursion_warnings = if self.has_active_rules(&options)
911            && data.iter().any(|q| !q.graph_name.is_default_graph())
912        {
913            let mut working = with_union(data);
914            let warnings = self.expand_rules_inner(&mut working, options, Some(&mut additions))?;
915            data.extend(additions.iter());
916            warnings
917        } else {
918            self.expand_rules_inner(data, options, Some(&mut additions))?
919        };
920        Ok(RuleExpansion {
921            graph: additions,
922            recursion_warnings,
923        })
924    }
925
926    fn expand_rules_inner(
927        &self,
928        data: &mut Dataset,
929        options: Options,
930        mut delta: Option<&mut Dataset>,
931    ) -> Result<Vec<String>> {
932        if options.max_rule_iterations == 0 {
933            return Err(Error::Invalid(
934                "max_rule_iterations must be positive".into(),
935            ));
936        }
937        let mut evaluation = Evaluation::new(options.max_validation_depth);
938        for &index in &self.execution_order {
939            let shape = &self.shapes[index];
940            if !options.use_shapes.is_empty() && !options.use_shapes.contains(&shape.node) {
941                continue;
942            }
943            if shape.rules.is_empty() {
944                continue;
945            }
946            let mut pass = 0;
947            loop {
948                if pass == options.max_rule_iterations {
949                    return Err(Error::Runtime(format!(
950                        "Rule iteration limit ({}) exceeded for {}",
951                        options.max_rule_iterations, shape.node
952                    )));
953                }
954                pass += 1;
955                let mut changed = false;
956                for rule in &shape.rules {
957                    if rule.deactivated {
958                        continue;
959                    }
960                    let focus =
961                        self.selected_focus_nodes(shape, data, &options, &mut evaluation)?;
962                    let functions = self.functions.for_dataset(data);
963                    evaluation.functions = functions.clone();
964                    let mut additions = HashSet::new();
965                    for node in focus {
966                        let mut condition_results = Vec::new();
967                        for &condition in &rule.conditions {
968                            self.evaluate_batch(
969                                condition,
970                                std::slice::from_ref(&node),
971                                data,
972                                &mut condition_results,
973                                &mut evaluation,
974                            )?;
975                        }
976                        if !condition_results.is_empty() {
977                            continue;
978                        }
979                        if let Some(expressions) = &rule.triple {
980                            let mut filter = |i, n: &Term| {
981                                let mut results = Vec::new();
982                                self.evaluate_batch(
983                                    i,
984                                    std::slice::from_ref(n),
985                                    data,
986                                    &mut results,
987                                    &mut evaluation,
988                                )?;
989                                Ok(results.is_empty())
990                            };
991                            let subjects =
992                                expressions[0].evaluate(&node, data, &functions, &mut filter)?;
993                            let predicates =
994                                expressions[1].evaluate(&node, data, &functions, &mut filter)?;
995                            let objects =
996                                expressions[2].evaluate(&node, data, &functions, &mut filter)?;
997                            for s in subjects {
998                                for p in &predicates {
999                                    for o in &objects {
1000                                        additions.insert(Quad::new(
1001                                            subject(&s)?,
1002                                            crate::constraints::named(
1003                                                p.clone(),
1004                                                "TripleRule predicate",
1005                                            )?,
1006                                            o.clone(),
1007                                            GraphName::DefaultGraph,
1008                                        ));
1009                                    }
1010                                }
1011                            }
1012                        }
1013                        for query in &rule.queries {
1014                            let result = query
1015                                .prepare(&functions, data)
1016                                .on_queryable_dataset(&*data)
1017                                .substitute_variable(Variable::new_unchecked("this"), node.clone())
1018                                .execute()?;
1019                            let QueryResults::Graph(triples) = result else {
1020                                return Err(Error::Invalid(
1021                                    "SPARQL rule did not return CONSTRUCT triples".into(),
1022                                ));
1023                            };
1024                            for triple in triples {
1025                                let triple = triple?;
1026                                additions.insert(Quad::new(
1027                                    triple.subject,
1028                                    triple.predicate,
1029                                    triple.object,
1030                                    GraphName::DefaultGraph,
1031                                ));
1032                            }
1033                        }
1034                    }
1035                    // All focus nodes of a rule see the same snapshot; subsequent
1036                    // rules see the complete batch of additions.
1037                    let mut rule_changed = false;
1038                    for quad in additions {
1039                        if data.insert(&quad) {
1040                            rule_changed = true;
1041                            if let Some(delta) = delta.as_deref_mut() {
1042                                delta.insert(&quad);
1043                            }
1044                            changed = true;
1045                        }
1046                    }
1047                    if rule_changed {
1048                        evaluation.clear_traversals();
1049                    }
1050                }
1051                if !options.iterate_rules || !changed {
1052                    break;
1053                }
1054            }
1055        }
1056        Ok(evaluation.warnings)
1057    }
1058
1059    fn selected_focus_nodes(
1060        &self,
1061        shape: &Shape,
1062        data: &Dataset,
1063        options: &Options,
1064        evaluation: &mut Evaluation,
1065    ) -> Result<Vec<Term>> {
1066        if !options.use_shapes.is_empty() && !options.focus_nodes.is_empty() {
1067            return Ok(options.focus_nodes.clone());
1068        }
1069        let mut nodes = self.focus_nodes(shape, data, evaluation)?;
1070        if !options.focus_nodes.is_empty() {
1071            nodes.retain(|node| options.focus_nodes.contains(node));
1072        }
1073        Ok(nodes)
1074    }
1075
1076    /// Expand rules, then discover validation targets on the resulting graph.
1077    pub fn validate(&self, data: &mut Dataset, options: Options) -> Result<ValidationReport> {
1078        let warnings = self.expand_rules(data, options.clone())?;
1079        self.validate_expanded(data, options, warnings)
1080    }
1081
1082    /// Validate a borrowed dataset without copying it or permitting rule mutation.
1083    /// Named-graph union materialization remains local to this validation call.
1084    pub fn validate_readonly(&self, data: &Dataset, options: Options) -> Result<ValidationReport> {
1085        if options.max_rule_iterations == 0 {
1086            return Err(Error::Invalid(
1087                "max_rule_iterations must be positive".into(),
1088            ));
1089        }
1090        if self.has_active_rules(&options) {
1091            return Err(Error::Invalid(
1092                "Readonly validation cannot execute active rules".into(),
1093            ));
1094        }
1095        self.validate_expanded(data, options, Vec::new())
1096    }
1097
1098    fn validate_expanded(
1099        &self,
1100        data: &Dataset,
1101        options: Options,
1102        warnings: Vec<String>,
1103    ) -> Result<ValidationReport> {
1104        let mut evaluation = Evaluation::new(options.max_validation_depth);
1105        evaluation.warnings = warnings;
1106        let display_data = data;
1107        let joined;
1108        let data = if data.iter().any(|q| !q.graph_name.is_default_graph()) {
1109            joined = with_union(data);
1110            &joined
1111        } else {
1112            data
1113        };
1114        evaluation.functions = self.functions.for_dataset(data);
1115        let mut violations = Vec::new();
1116        for &index in &self.execution_order {
1117            let shape = &self.shapes[index];
1118            if !options.use_shapes.is_empty() && !options.use_shapes.contains(&shape.node) {
1119                continue;
1120            }
1121            if shape.deactivated {
1122                continue;
1123            }
1124            let focus_nodes = self.selected_focus_nodes(shape, data, &options, &mut evaluation)?;
1125            self.evaluate_batch(index, &focus_nodes, data, &mut violations, &mut evaluation)?;
1126            if options.abort_on_first && !violations.is_empty() {
1127                break;
1128            }
1129        }
1130        let conforms = violations.iter().all(|v| {
1131            let severity = &self.shapes[v.shape].severity;
1132            (options.allow_warnings && severity == &iri(SH, "Warning"))
1133                || ((options.allow_infos || options.allow_warnings) && severity == &iri(SH, "Info"))
1134        });
1135        let mut report = Dataset::new();
1136        let root: Term = BlankNode::default().into();
1137        add(
1138            &mut report,
1139            &root,
1140            iri(RDF, "type"),
1141            iri(SH, "ValidationReport").into(),
1142        )?;
1143        add(
1144            &mut report,
1145            &root,
1146            iri(SH, "conforms"),
1147            Literal::from(conforms).into(),
1148        )?;
1149        let mut text = format!(
1150            "Validation Report\nConforms: {}\n",
1151            if conforms { "True" } else { "False" }
1152        );
1153        if !violations.is_empty() {
1154            text.push_str(&format!("Results ({}):\n", violations.len()));
1155        }
1156        let mut shape_namespaces = self.namespaces.clone();
1157        let mut data_namespaces = options.namespaces;
1158        for (p, ns) in [("sh", SH), ("rdf", RDF), ("rdfs", RDFS), ("xsd", XSD)] {
1159            shape_namespaces.push((p.into(), ns.into()));
1160            data_namespaces.push((p.into(), ns.into()));
1161        }
1162        let display = crate::display::Display {
1163            shape_namespaces,
1164            data_namespaces,
1165            data: display_data,
1166            shapes: &self.graph,
1167        };
1168        let mut clones = ReportClones::default();
1169        let mut descriptions = Vec::new();
1170        for violation in &violations {
1171            let mut description = String::new();
1172            self.write_result(
1173                &mut report,
1174                &root,
1175                "result",
1176                violation,
1177                data,
1178                &mut description,
1179                &mut clones,
1180                &display,
1181            )?;
1182            descriptions.push(description);
1183        }
1184        descriptions.sort();
1185        for description in descriptions {
1186            text.push_str(&description);
1187        }
1188        Ok(ValidationReport {
1189            conforms,
1190            graph: report,
1191            text,
1192            recursion_warnings: evaluation.warnings,
1193        })
1194    }
1195    #[allow(clippy::too_many_arguments)]
1196    fn write_result(
1197        &self,
1198        report: &mut Dataset,
1199        parent: &Term,
1200        edge: &str,
1201        violation: &Violation,
1202        data: &Dataset,
1203        text: &mut String,
1204        clones: &mut ReportClones,
1205        display: &crate::display::Display<'_>,
1206    ) -> Result<()> {
1207        let shape = &self.shapes[violation.shape];
1208        let result: Term = BlankNode::default().into();
1209        add(report, parent, iri(SH, edge), result.clone())?;
1210        for (predicate, object) in [
1211            (iri(RDF, "type"), iri(SH, "ValidationResult").into()),
1212            (
1213                iri(SH, "sourceConstraintComponent"),
1214                violation.component.clone().into(),
1215            ),
1216            (iri(SH, "sourceShape"), shape.node.clone()),
1217            (iri(SH, "resultSeverity"), shape.severity.clone().into()),
1218            (iri(SH, "focusNode"), violation.focus.clone()),
1219        ] {
1220            add(report, &result, predicate, object)?;
1221        }
1222        clones.copy(&self.graph, report, &shape.node)?;
1223        clones.copy(data, report, &violation.focus)?;
1224        if let Some(path) = &violation.path {
1225            add(report, &result, iri(SH, "resultPath"), path.clone())?;
1226            clones.copy(&self.graph, report, path)?;
1227        }
1228        if let Some(value) = &violation.value {
1229            add(report, &result, iri(SH, "value"), value.clone())?;
1230            clones.copy(data, report, value)?;
1231        }
1232        if let Some(constraint) = &violation.source_constraint {
1233            add(
1234                report,
1235                &result,
1236                iri(SH, "sourceConstraint"),
1237                constraint.clone(),
1238            )?;
1239            clones.copy(&self.graph, report, constraint)?;
1240        }
1241        let messages = if let Some(messages) = &violation.messages {
1242            messages.clone()
1243        } else if shape.messages.is_empty() {
1244            let message = if !violation.message.is_empty() {
1245                violation.message.clone()
1246            } else {
1247                let focus = display.term(&violation.focus, false);
1248                let component = violation
1249                    .component
1250                    .as_str()
1251                    .strip_prefix(SH)
1252                    .unwrap_or(violation.component.as_str());
1253                match component {
1254                    "MinCountConstraintComponent" | "MaxCountConstraintComponent" => {
1255                        let minimum = component == "MinCountConstraintComponent";
1256                        let count = shape
1257                            .constraints
1258                            .iter()
1259                            .find_map(|constraint| match constraint {
1260                                Constraint::Count {
1261                                    minimum: is_minimum,
1262                                    count,
1263                                } if *is_minimum == minimum => Some(count),
1264                                _ => None,
1265                            })
1266                            .expect("cardinality violation requires a compiled count constraint");
1267                        let direction = if minimum { "Less" } else { "More" };
1268                        let location = if let Some(path) = &violation.path {
1269                            format!("{focus}->{}", display.term(path, true))
1270                        } else {
1271                            focus
1272                        };
1273                        format!("{direction} than {count} values on {location}")
1274                    }
1275                    "MinLengthConstraintComponent" | "MaxLengthConstraintComponent" => {
1276                        let minimum = component == "MinLengthConstraintComponent";
1277                        let count = shape
1278                            .constraints
1279                            .iter()
1280                            .find_map(|constraint| match constraint {
1281                                Constraint::Length {
1282                                    minimum: is_minimum,
1283                                    count,
1284                                } if *is_minimum == minimum => Some(count),
1285                                _ => None,
1286                            })
1287                            .expect("length violation requires a compiled length constraint");
1288                        let number = count.number.to_string();
1289                        let lexical = count.literal.value();
1290                        let value = if lexical == number {
1291                            format!("\"{lexical}\"")
1292                        } else {
1293                            format!("\"{lexical}\" = {number}")
1294                        };
1295                        let operator = if minimum { ">=" } else { "<=" };
1296                        format!(
1297                            "String length not {operator} Literal({value}, datatype={})",
1298                            display.iri(&count.literal.datatype().into_owned())
1299                        )
1300                    }
1301                    "QualifiedMinCountConstraintComponent"
1302                    | "QualifiedMaxCountConstraintComponent" => {
1303                        let (target, minimum, maximum) = shape
1304                            .constraints
1305                            .iter()
1306                            .find_map(|constraint| match constraint {
1307                                Constraint::Qualified {
1308                                    shape,
1309                                    minimum,
1310                                    maximum,
1311                                    ..
1312                                } => Some((*shape, minimum, maximum)),
1313                                _ => None,
1314                            })
1315                            .expect("qualified violation requires a compiled qualified constraint");
1316                        let mut counts = String::new();
1317                        if let Some(minimum) = minimum {
1318                            counts.push_str(&format!(" MinCount {minimum}"));
1319                        }
1320                        if let Some(maximum) = maximum {
1321                            counts.push_str(&format!(" MaxCount {maximum}"));
1322                        }
1323                        format!(
1324                            "Focus node does not conform to shape{counts}: {}",
1325                            display.term(&self.shapes[target].node, true)
1326                        )
1327                    }
1328                    "PatternConstraintComponent" => {
1329                        let patterns = values(&self.graph, &shape.node, "pattern")?
1330                            .iter()
1331                            .filter_map(crate::value::lexical)
1332                            .map(str::to_owned)
1333                            .collect::<Vec<_>>();
1334                        if patterns.len() == 1 {
1335                            format!("Value does not match pattern '{}'", patterns[0])
1336                        } else {
1337                            format!(
1338                                "Value does not match every pattern in ('{}')",
1339                                patterns.join("', '")
1340                            )
1341                        }
1342                    }
1343                    "ClassConstraintComponent" => {
1344                        let mut classes = values(&self.graph, &shape.node, "class")?
1345                            .iter()
1346                            .map(|n| display.term(n, true))
1347                            .collect::<Vec<_>>();
1348                        classes.sort();
1349                        if classes.len() == 1 {
1350                            format!("Value does not have class {}", classes[0])
1351                        } else {
1352                            format!("Value class is not in classes ({})", classes.join(", "))
1353                        }
1354                    }
1355                    "NodeConstraintComponent" => {
1356                        let mut nodes = values(&self.graph, &shape.node, "node")?
1357                            .iter()
1358                            .map(|n| format!("'{}'", display.term(n, true)))
1359                            .collect::<Vec<_>>();
1360                        nodes.sort();
1361                        if nodes.len() == 1 {
1362                            format!(
1363                                "Value does not conform to Shape {}. See details for more information.",
1364                                nodes[0]
1365                            )
1366                        } else {
1367                            format!(
1368                                "Value must conform to every Shape in ({})",
1369                                nodes.join(", ")
1370                            )
1371                        }
1372                    }
1373                    "NotConstraintComponent" => {
1374                        let mut nodes = values(&self.graph, &shape.node, "not")?
1375                            .iter()
1376                            .map(|n| display.term(n, true))
1377                            .collect::<Vec<_>>();
1378                        nodes.sort();
1379                        format!(
1380                            "Node {focus} must not conform to shape {}",
1381                            nodes.join(", ")
1382                        )
1383                    }
1384                    _ => format!("{component} violated at {focus}"),
1385                }
1386            };
1387            vec![Literal::new_simple_literal(message).into()]
1388        } else {
1389            shape.messages.clone()
1390        };
1391        let component = violation
1392            .component
1393            .as_str()
1394            .strip_prefix(SH)
1395            .unwrap_or(violation.component.as_str());
1396        let title = if [
1397            "QualifiedMinCountConstraintComponent",
1398            "QualifiedMaxCountConstraintComponent",
1399        ]
1400        .contains(&component)
1401        {
1402            "QualifiedValueShapeConstraintComponent"
1403        } else {
1404            component
1405        };
1406        let category = if shape.severity == iri(SH, "Violation") {
1407            "Constraint Violation"
1408        } else {
1409            "Validation Result"
1410        };
1411        text.push_str(&format!(
1412            "{category} in {title} ({}):\n\tSeverity: {}\n\tSource Shape: {}\n\tFocus Node: {}\n",
1413            violation.component.as_str(),
1414            display.iri(&shape.severity),
1415            display.term(&shape.node, true),
1416            display.term(&violation.focus, false)
1417        ));
1418        if let Some(value) = &violation.value {
1419            text.push_str(&format!("\tValue Node: {}\n", display.term(value, false)));
1420        }
1421        if let Some(path) = &violation.path {
1422            text.push_str(&format!("\tResult Path: {}\n", display.term(path, true)));
1423        }
1424        if let Some(source) = &violation.source_constraint {
1425            text.push_str(&format!(
1426                "\tSource Constraint: {}\n",
1427                display.term(source, true)
1428            ));
1429        }
1430        let mut messages = messages;
1431        messages.sort_by_key(ToString::to_string);
1432        for message in messages {
1433            let printed = match &message {
1434                Term::Literal(l) => l.value().to_owned(),
1435                _ => message.to_string(),
1436            };
1437            text.push_str(&format!("\tMessage: {printed}\n"));
1438            add(report, &result, iri(SH, "resultMessage"), message)?;
1439        }
1440        let mut descriptions = Vec::new();
1441        for detail in &violation.details {
1442            let mut description = String::new();
1443            self.write_result(
1444                report,
1445                &result,
1446                "detail",
1447                detail,
1448                data,
1449                &mut description,
1450                clones,
1451                display,
1452            )?;
1453            descriptions.push(description);
1454        }
1455        descriptions.sort();
1456        if !descriptions.is_empty() {
1457            text.push_str("\tDetails:\n");
1458        }
1459        for description in descriptions {
1460            for line in description.lines() {
1461                text.push_str(&format!("\t\t{line}\n"));
1462            }
1463        }
1464        Ok(())
1465    }
1466}
1467
1468fn is_instance(data: &Dataset, value: &Term, class: &Term) -> Result<bool> {
1469    let mut pending = objects(data, value, &iri(RDF, "type"))?;
1470    let mut seen = HashSet::new();
1471    while let Some(current) = pending.pop() {
1472        if &current == class {
1473            return Ok(true);
1474        }
1475        if seen.insert(current.clone()) {
1476            pending.extend(objects(data, &current, &iri(RDFS, "subClassOf"))?);
1477        }
1478    }
1479    Ok(false)
1480}
1481
1482fn store_snapshot(store: &oxigraph::store::Store) -> Result<Dataset> {
1483    store
1484        .iter()
1485        .map(|q| q.map(store_quad))
1486        .collect::<std::result::Result<Dataset, _>>()
1487        .map_err(|e| Error::Runtime(format!("Store snapshot failed: {e}")))
1488}
1489
1490fn commit_store(store: &oxigraph::store::Store, before: &Dataset, after: &Dataset) -> Result<()> {
1491    store
1492        .extend(
1493            after
1494                .iter()
1495                .filter(|q| !before.contains(*q))
1496                .map(|q| q.into_owned()),
1497        )
1498        .map_err(|e| Error::Runtime(format!("Store commit failed: {e}")))
1499}
1500
1501#[cfg(test)]
1502mod union_tests {
1503    use super::*;
1504    use oxigraph::io::RdfFormat;
1505
1506    #[test]
1507    fn validation_without_active_rules_builds_only_its_validation_union() {
1508        let shapes = parse("@prefix sh:<http://www.w3.org/ns/shacl#> . <urn:S> sh:targetNode <urn:s>; sh:path <urn:p>; sh:minCount 1.", RdfFormat::Turtle, None).unwrap();
1509        let program = CompiledShapes::compile(shapes, true).unwrap();
1510        let mut data =
1511            parse_dataset("<urn:g> { <urn:s> <urn:p> 1 }", RdfFormat::TriG, None).unwrap();
1512        UNION_BUILDS.with(|count| count.set(0));
1513        assert!(
1514            program
1515                .validate(&mut data, Options::default())
1516                .unwrap()
1517                .conforms
1518        );
1519        assert_eq!(UNION_BUILDS.with(std::cell::Cell::get), 1);
1520        assert_eq!(data.len(), 1);
1521        assert!(!data.iter().any(|q| q.graph_name.is_default_graph()));
1522        assert!(
1523            program
1524                .expand_rules(
1525                    &mut data,
1526                    Options {
1527                        max_rule_iterations: 0,
1528                        ..Options::default()
1529                    }
1530                )
1531                .is_err()
1532        );
1533    }
1534}
1535
1536#[cfg(test)]
1537mod traversal_tests {
1538    use super::*;
1539    use crate::path::{PATH_EVALUATIONS, Path};
1540
1541    #[test]
1542    fn traversal_cache_retention_is_bounded() {
1543        let mut evaluation = Evaluation::new(15);
1544        let path = Path::OneOrMore(Box::new(Path::Predicate(iri("urn:", "p"))));
1545        let data = Dataset::new();
1546        for index in 0..1025 {
1547            let focus = Term::from(iri("urn:node:", &index.to_string()));
1548            assert!(evaluation.values(&path, &data, &focus).unwrap().is_empty());
1549            assert!(evaluation.path_values.len() <= 1024);
1550        }
1551        assert!(!evaluation.reserve_traversal(32769));
1552        evaluation.clear_traversals();
1553        assert!(evaluation.path_values.is_empty());
1554        assert_eq!(evaluation.cached_terms, 0);
1555    }
1556
1557    #[test]
1558    fn property_paths_are_reused_only_until_the_phase_changes() {
1559        let mut evaluation = Evaluation::new(15);
1560        let path = Path::OneOrMore(Box::new(Path::Predicate(iri("urn:", "next"))));
1561        let focus = Term::from(iri("urn:", "s"));
1562        let mut data = parse(
1563            "<urn:s> <urn:next> <urn:t> .",
1564            oxigraph::io::RdfFormat::Turtle,
1565            None,
1566        )
1567        .unwrap();
1568        PATH_EVALUATIONS.with(|count| count.set(0));
1569        assert_eq!(evaluation.values(&path, &data, &focus).unwrap().len(), 1);
1570        assert_eq!(evaluation.values(&path, &data, &focus).unwrap().len(), 1);
1571        assert_eq!(PATH_EVALUATIONS.with(std::cell::Cell::get), 1);
1572        data.insert(
1573            Quad::new(
1574                iri("urn:", "t"),
1575                iri("urn:", "next"),
1576                iri("urn:", "u"),
1577                GraphName::DefaultGraph,
1578            )
1579            .as_ref(),
1580        );
1581        evaluation.clear_traversals();
1582        assert_eq!(evaluation.values(&path, &data, &focus).unwrap().len(), 2);
1583        assert_eq!(PATH_EVALUATIONS.with(std::cell::Cell::get), 2);
1584    }
1585}