Skip to main content

oxirs_core/query/
property_paths.rs

1//! SPARQL 1.2 Property Paths implementation
2//!
3//! This module implements enhanced property paths for SPARQL 1.2,
4//! allowing complex graph navigation patterns.
5
6#![allow(dead_code)]
7
8use crate::model::{NamedNode, Term, Variable};
9use crate::query::algebra::{TermPattern, TriplePattern};
10use crate::OxirsError;
11use std::collections::HashSet;
12use std::fmt;
13
14/// Property path expression
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum PropertyPath {
17    /// Direct predicate (e.g., `:knows`)
18    Predicate(NamedNode),
19
20    /// Inverse path (e.g., `^:knows`)
21    Inverse(Box<PropertyPath>),
22
23    /// Sequence of paths (e.g., `:knows/:worksFor`)
24    Sequence(Box<PropertyPath>, Box<PropertyPath>),
25
26    /// Alternative paths (e.g., `:knows|:friendOf`)
27    Alternative(Box<PropertyPath>, Box<PropertyPath>),
28
29    /// Zero or more repetitions (e.g., `:knows*`)
30    ZeroOrMore(Box<PropertyPath>),
31
32    /// One or more repetitions (e.g., `:knows+`)
33    OneOrMore(Box<PropertyPath>),
34
35    /// Zero or one occurrence (e.g., `:knows?`)
36    ZeroOrOne(Box<PropertyPath>),
37
38    /// Negated property set (e.g., `!(:knows|:hates)`)
39    NegatedPropertySet(Vec<NamedNode>),
40
41    /// Fixed length path (SPARQL 1.2 extension)
42    FixedLength(Box<PropertyPath>, usize),
43
44    /// Range length path (SPARQL 1.2 extension)
45    RangeLength(Box<PropertyPath>, usize, Option<usize>),
46
47    /// Distinct path (SPARQL 1.2 extension)
48    Distinct(Box<PropertyPath>),
49}
50
51impl PropertyPath {
52    /// Create a simple predicate path
53    pub fn predicate(iri: NamedNode) -> Self {
54        PropertyPath::Predicate(iri)
55    }
56
57    /// Create an inverse path
58    pub fn inverse(path: PropertyPath) -> Self {
59        PropertyPath::Inverse(Box::new(path))
60    }
61
62    /// Create a sequence path
63    pub fn sequence(left: PropertyPath, right: PropertyPath) -> Self {
64        PropertyPath::Sequence(Box::new(left), Box::new(right))
65    }
66
67    /// Create an alternative path
68    pub fn alternative(left: PropertyPath, right: PropertyPath) -> Self {
69        PropertyPath::Alternative(Box::new(left), Box::new(right))
70    }
71
72    /// Create a zero-or-more path
73    pub fn zero_or_more(path: PropertyPath) -> Self {
74        PropertyPath::ZeroOrMore(Box::new(path))
75    }
76
77    /// Create a one-or-more path
78    pub fn one_or_more(path: PropertyPath) -> Self {
79        PropertyPath::OneOrMore(Box::new(path))
80    }
81
82    /// Create a zero-or-one path
83    pub fn zero_or_one(path: PropertyPath) -> Self {
84        PropertyPath::ZeroOrOne(Box::new(path))
85    }
86
87    /// Create a negated property set
88    pub fn negated_set(predicates: Vec<NamedNode>) -> Self {
89        PropertyPath::NegatedPropertySet(predicates)
90    }
91
92    /// Create a fixed length path (SPARQL 1.2)
93    pub fn fixed_length(path: PropertyPath, n: usize) -> Self {
94        PropertyPath::FixedLength(Box::new(path), n)
95    }
96
97    /// Create a range length path (SPARQL 1.2)
98    pub fn range_length(path: PropertyPath, min: usize, max: Option<usize>) -> Self {
99        PropertyPath::RangeLength(Box::new(path), min, max)
100    }
101
102    /// Create a distinct path (SPARQL 1.2)
103    pub fn distinct(path: PropertyPath) -> Self {
104        PropertyPath::Distinct(Box::new(path))
105    }
106
107    /// Check if this path is simple (just a predicate)
108    pub fn is_simple(&self) -> bool {
109        matches!(self, PropertyPath::Predicate(_))
110    }
111
112    /// Get the minimum length of this path
113    pub fn min_length(&self) -> usize {
114        match self {
115            PropertyPath::Predicate(_) => 1,
116            PropertyPath::Inverse(p) => p.min_length(),
117            PropertyPath::Sequence(l, r) => l.min_length() + r.min_length(),
118            PropertyPath::Alternative(l, r) => l.min_length().min(r.min_length()),
119            PropertyPath::ZeroOrMore(_) => 0,
120            PropertyPath::OneOrMore(p) => p.min_length(),
121            PropertyPath::ZeroOrOne(_) => 0,
122            PropertyPath::NegatedPropertySet(_) => 1,
123            PropertyPath::FixedLength(_, n) => *n,
124            PropertyPath::RangeLength(_, min, _) => *min,
125            PropertyPath::Distinct(p) => p.min_length(),
126        }
127    }
128
129    /// Get the maximum length of this path (None = unbounded)
130    pub fn max_length(&self) -> Option<usize> {
131        match self {
132            PropertyPath::Predicate(_) => Some(1),
133            PropertyPath::Inverse(p) => p.max_length(),
134            PropertyPath::Sequence(l, r) => match (l.max_length(), r.max_length()) {
135                (Some(a), Some(b)) => Some(a + b),
136                _ => None,
137            },
138            PropertyPath::Alternative(l, r) => match (l.max_length(), r.max_length()) {
139                (Some(a), Some(b)) => Some(a.max(b)),
140                _ => None,
141            },
142            PropertyPath::ZeroOrMore(_) => None,
143            PropertyPath::OneOrMore(_) => None,
144            PropertyPath::ZeroOrOne(p) => p.max_length().map(|_| 1),
145            PropertyPath::NegatedPropertySet(_) => Some(1),
146            PropertyPath::FixedLength(_, n) => Some(*n),
147            PropertyPath::RangeLength(_, _, max) => *max,
148            PropertyPath::Distinct(p) => p.max_length(),
149        }
150    }
151
152    /// Collect all predicates mentioned in this path
153    pub fn predicates(&self) -> HashSet<&NamedNode> {
154        let mut predicates = HashSet::new();
155        self.collect_predicates(&mut predicates);
156        predicates
157    }
158
159    fn collect_predicates<'a>(&'a self, predicates: &mut HashSet<&'a NamedNode>) {
160        match self {
161            PropertyPath::Predicate(p) => {
162                predicates.insert(p);
163            }
164            PropertyPath::Inverse(p) => p.collect_predicates(predicates),
165            PropertyPath::Sequence(l, r) => {
166                l.collect_predicates(predicates);
167                r.collect_predicates(predicates);
168            }
169            PropertyPath::Alternative(l, r) => {
170                l.collect_predicates(predicates);
171                r.collect_predicates(predicates);
172            }
173            PropertyPath::ZeroOrMore(p)
174            | PropertyPath::OneOrMore(p)
175            | PropertyPath::ZeroOrOne(p)
176            | PropertyPath::Distinct(p) => p.collect_predicates(predicates),
177            PropertyPath::FixedLength(p, _) | PropertyPath::RangeLength(p, _, _) => {
178                p.collect_predicates(predicates)
179            }
180            PropertyPath::NegatedPropertySet(ps) => {
181                for p in ps {
182                    predicates.insert(p);
183                }
184            }
185        }
186    }
187}
188
189impl fmt::Display for PropertyPath {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        match self {
192            PropertyPath::Predicate(p) => write!(f, "{p}"),
193            PropertyPath::Inverse(p) => write!(f, "^{p}"),
194            PropertyPath::Sequence(l, r) => write!(f, "{l}/{r}"),
195            PropertyPath::Alternative(l, r) => write!(f, "{l}|{r}"),
196            PropertyPath::ZeroOrMore(p) => write!(f, "{p}*"),
197            PropertyPath::OneOrMore(p) => write!(f, "{p}+"),
198            PropertyPath::ZeroOrOne(p) => write!(f, "{p}?"),
199            PropertyPath::NegatedPropertySet(ps) => {
200                write!(f, "!(")?;
201                for (i, p) in ps.iter().enumerate() {
202                    if i > 0 {
203                        write!(f, "|")?;
204                    }
205                    write!(f, "{p}")?;
206                }
207                write!(f, ")")
208            }
209            PropertyPath::FixedLength(p, n) => write!(f, "{p}{{{n}}}"),
210            PropertyPath::RangeLength(p, min, max) => match max {
211                Some(m) => write!(f, "{p}{{{min},{m}}}"),
212                None => write!(f, "{p}{{{min},}}"),
213            },
214            PropertyPath::Distinct(p) => write!(f, "DISTINCT({p})"),
215        }
216    }
217}
218
219/// Property path pattern for use in queries
220#[derive(Debug, Clone, PartialEq, Eq, Hash)]
221pub struct PropertyPathPattern {
222    /// Subject of the path
223    pub subject: TermPattern,
224    /// The property path
225    pub path: PropertyPath,
226    /// Object of the path
227    pub object: TermPattern,
228}
229
230impl PropertyPathPattern {
231    /// Create a new property path pattern
232    pub fn new(subject: TermPattern, path: PropertyPath, object: TermPattern) -> Self {
233        PropertyPathPattern {
234            subject,
235            path,
236            object,
237        }
238    }
239
240    /// Convert a simple property path to a regular triple pattern
241    pub fn to_triple_pattern(&self) -> Option<TriplePattern> {
242        use crate::model::pattern::{ObjectPattern, PredicatePattern, SubjectPattern};
243
244        match &self.path {
245            PropertyPath::Predicate(p) => {
246                let subject = match &self.subject {
247                    TermPattern::Variable(v) => Some(SubjectPattern::Variable(v.clone())),
248                    TermPattern::NamedNode(n) => Some(SubjectPattern::NamedNode(n.clone())),
249                    TermPattern::BlankNode(b) => Some(SubjectPattern::BlankNode(b.clone())),
250                    _ => None,
251                };
252
253                let predicate = Some(PredicatePattern::NamedNode(p.clone()));
254
255                let object = match &self.object {
256                    TermPattern::Variable(v) => Some(ObjectPattern::Variable(v.clone())),
257                    TermPattern::NamedNode(n) => Some(ObjectPattern::NamedNode(n.clone())),
258                    TermPattern::BlankNode(b) => Some(ObjectPattern::BlankNode(b.clone())),
259                    TermPattern::Literal(l) => Some(ObjectPattern::Literal(l.clone())),
260                    // RDF-star quoted triples cannot be expressed as a simple
261                    // object pattern here; return None so the caller falls back
262                    // to the general path evaluator instead of panicking.
263                    TermPattern::QuotedTriple(_) => return None,
264                };
265
266                Some(TriplePattern {
267                    subject,
268                    predicate,
269                    object,
270                })
271            }
272            _ => None,
273        }
274    }
275
276    /// Check if this pattern contains variables
277    pub fn has_variables(&self) -> bool {
278        self.subject.is_variable() || self.object.is_variable()
279    }
280
281    /// Get all variables in this pattern
282    pub fn variables(&self) -> Vec<Variable> {
283        let mut vars = Vec::new();
284        if let TermPattern::Variable(v) = &self.subject {
285            vars.push(v.clone());
286        }
287        if let TermPattern::Variable(v) = &self.object {
288            vars.push(v.clone());
289        }
290        vars
291    }
292}
293
294/// Property path evaluator
295pub struct PropertyPathEvaluator {
296    /// Maximum depth for recursive paths
297    max_depth: usize,
298    /// Enable cycle detection
299    cycle_detection: bool,
300    /// Enable distinct paths (SPARQL 1.2)
301    distinct_paths: bool,
302}
303
304impl Default for PropertyPathEvaluator {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310impl PropertyPathEvaluator {
311    /// Create a new evaluator with default settings
312    pub fn new() -> Self {
313        PropertyPathEvaluator {
314            max_depth: 100,
315            cycle_detection: true,
316            distinct_paths: false,
317        }
318    }
319
320    /// Set maximum recursion depth
321    pub fn with_max_depth(mut self, depth: usize) -> Self {
322        self.max_depth = depth;
323        self
324    }
325
326    /// Enable or disable cycle detection
327    pub fn with_cycle_detection(mut self, enable: bool) -> Self {
328        self.cycle_detection = enable;
329        self
330    }
331
332    /// Enable distinct paths (SPARQL 1.2)
333    pub fn with_distinct_paths(mut self, enable: bool) -> Self {
334        self.distinct_paths = enable;
335        self
336    }
337
338    /// Evaluate a property path pattern
339    /// This is a placeholder - actual implementation would query the graph
340    pub fn evaluate(
341        &self,
342        _pattern: &PropertyPathPattern,
343    ) -> Result<Vec<(Term, Term)>, OxirsError> {
344        // Placeholder implementation
345        Ok(Vec::new())
346    }
347}
348
349/// Property path optimizer for query planning
350pub struct PropertyPathOptimizer {
351    /// Enable path rewriting
352    rewrite_enabled: bool,
353    /// Enable path decomposition
354    decompose_enabled: bool,
355}
356
357impl Default for PropertyPathOptimizer {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363impl PropertyPathOptimizer {
364    /// Create new optimizer
365    pub fn new() -> Self {
366        PropertyPathOptimizer {
367            rewrite_enabled: true,
368            decompose_enabled: true,
369        }
370    }
371
372    /// Optimize a property path
373    pub fn optimize(&self, path: PropertyPath) -> PropertyPath {
374        if !self.rewrite_enabled {
375            return path;
376        }
377
378        // Apply optimization rules
379        self.optimize_recursive(path)
380    }
381
382    #[allow(clippy::only_used_in_recursion)]
383    fn optimize_recursive(&self, path: PropertyPath) -> PropertyPath {
384        match path {
385            // Optimize p/p to p{2}
386            PropertyPath::Sequence(ref l, ref r) if l == r => {
387                PropertyPath::FixedLength(l.clone(), 2)
388            }
389
390            // Optimize p? | p+ to p*
391            PropertyPath::Alternative(ref l, ref r) => match (l.as_ref(), r.as_ref()) {
392                (PropertyPath::ZeroOrOne(p1), PropertyPath::OneOrMore(p2)) if p1 == p2 => {
393                    PropertyPath::ZeroOrMore(p1.clone())
394                }
395                (PropertyPath::OneOrMore(p1), PropertyPath::ZeroOrOne(p2)) if p1 == p2 => {
396                    PropertyPath::ZeroOrMore(p1.clone())
397                }
398                _ => PropertyPath::Alternative(
399                    Box::new(self.optimize_recursive(*l.clone())),
400                    Box::new(self.optimize_recursive(*r.clone())),
401                ),
402            },
403
404            // Recursively optimize nested paths
405            PropertyPath::Inverse(p) => {
406                PropertyPath::Inverse(Box::new(self.optimize_recursive(*p)))
407            }
408            PropertyPath::Sequence(l, r) => PropertyPath::Sequence(
409                Box::new(self.optimize_recursive(*l)),
410                Box::new(self.optimize_recursive(*r)),
411            ),
412            PropertyPath::ZeroOrMore(p) => {
413                PropertyPath::ZeroOrMore(Box::new(self.optimize_recursive(*p)))
414            }
415            PropertyPath::OneOrMore(p) => {
416                PropertyPath::OneOrMore(Box::new(self.optimize_recursive(*p)))
417            }
418            PropertyPath::ZeroOrOne(p) => {
419                PropertyPath::ZeroOrOne(Box::new(self.optimize_recursive(*p)))
420            }
421            PropertyPath::FixedLength(p, n) => {
422                PropertyPath::FixedLength(Box::new(self.optimize_recursive(*p)), n)
423            }
424            PropertyPath::RangeLength(p, min, max) => {
425                PropertyPath::RangeLength(Box::new(self.optimize_recursive(*p)), min, max)
426            }
427            PropertyPath::Distinct(p) => {
428                PropertyPath::Distinct(Box::new(self.optimize_recursive(*p)))
429            }
430
431            // Base cases
432            PropertyPath::Predicate(_) | PropertyPath::NegatedPropertySet(_) => path,
433        }
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_property_path_creation() {
443        let p1 = NamedNode::new("http://example.org/knows").expect("valid IRI");
444        let p2 = NamedNode::new("http://example.org/likes").expect("valid IRI");
445
446        // Simple predicate
447        let path = PropertyPath::predicate(p1.clone());
448        assert_eq!(path.min_length(), 1);
449        assert_eq!(path.max_length(), Some(1));
450
451        // Sequence
452        let seq = PropertyPath::sequence(
453            PropertyPath::predicate(p1.clone()),
454            PropertyPath::predicate(p2.clone()),
455        );
456        assert_eq!(seq.min_length(), 2);
457        assert_eq!(seq.max_length(), Some(2));
458
459        // Zero or more
460        let star = PropertyPath::zero_or_more(PropertyPath::predicate(p1.clone()));
461        assert_eq!(star.min_length(), 0);
462        assert_eq!(star.max_length(), None);
463
464        // Fixed length
465        let fixed = PropertyPath::fixed_length(PropertyPath::predicate(p1.clone()), 3);
466        assert_eq!(fixed.min_length(), 3);
467        assert_eq!(fixed.max_length(), Some(3));
468    }
469
470    #[test]
471    fn test_property_path_display() {
472        let p1 = NamedNode::new("http://example.org/p").expect("valid IRI");
473        let p2 = NamedNode::new("http://example.org/q").expect("valid IRI");
474
475        let path = PropertyPath::sequence(
476            PropertyPath::predicate(p1.clone()),
477            PropertyPath::zero_or_more(PropertyPath::predicate(p2.clone())),
478        );
479
480        let expected = format!("{p1}/{p2}*");
481        assert_eq!(format!("{path}"), expected);
482    }
483
484    #[test]
485    fn test_path_optimization() {
486        let optimizer = PropertyPathOptimizer::new();
487        let p = PropertyPath::predicate(NamedNode::new("http://example.org/p").expect("valid IRI"));
488
489        // Optimize p/p to p{2}
490        let seq = PropertyPath::sequence(p.clone(), p.clone());
491        let optimized = optimizer.optimize(seq);
492        assert!(matches!(optimized, PropertyPath::FixedLength(_, 2)));
493
494        // Optimize p? | p+ to p*
495        let alt = PropertyPath::alternative(
496            PropertyPath::zero_or_one(p.clone()),
497            PropertyPath::one_or_more(p.clone()),
498        );
499        let optimized = optimizer.optimize(alt);
500        assert!(matches!(optimized, PropertyPath::ZeroOrMore(_)));
501    }
502}