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                    TermPattern::QuotedTriple(_) => {
261                        panic!("RDF-star quoted triples not yet supported in property paths")
262                    }
263                };
264
265                Some(TriplePattern {
266                    subject,
267                    predicate,
268                    object,
269                })
270            }
271            _ => None,
272        }
273    }
274
275    /// Check if this pattern contains variables
276    pub fn has_variables(&self) -> bool {
277        self.subject.is_variable() || self.object.is_variable()
278    }
279
280    /// Get all variables in this pattern
281    pub fn variables(&self) -> Vec<Variable> {
282        let mut vars = Vec::new();
283        if let TermPattern::Variable(v) = &self.subject {
284            vars.push(v.clone());
285        }
286        if let TermPattern::Variable(v) = &self.object {
287            vars.push(v.clone());
288        }
289        vars
290    }
291}
292
293/// Property path evaluator
294pub struct PropertyPathEvaluator {
295    /// Maximum depth for recursive paths
296    max_depth: usize,
297    /// Enable cycle detection
298    cycle_detection: bool,
299    /// Enable distinct paths (SPARQL 1.2)
300    distinct_paths: bool,
301}
302
303impl Default for PropertyPathEvaluator {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309impl PropertyPathEvaluator {
310    /// Create a new evaluator with default settings
311    pub fn new() -> Self {
312        PropertyPathEvaluator {
313            max_depth: 100,
314            cycle_detection: true,
315            distinct_paths: false,
316        }
317    }
318
319    /// Set maximum recursion depth
320    pub fn with_max_depth(mut self, depth: usize) -> Self {
321        self.max_depth = depth;
322        self
323    }
324
325    /// Enable or disable cycle detection
326    pub fn with_cycle_detection(mut self, enable: bool) -> Self {
327        self.cycle_detection = enable;
328        self
329    }
330
331    /// Enable distinct paths (SPARQL 1.2)
332    pub fn with_distinct_paths(mut self, enable: bool) -> Self {
333        self.distinct_paths = enable;
334        self
335    }
336
337    /// Evaluate a property path pattern
338    /// This is a placeholder - actual implementation would query the graph
339    pub fn evaluate(
340        &self,
341        _pattern: &PropertyPathPattern,
342    ) -> Result<Vec<(Term, Term)>, OxirsError> {
343        // Placeholder implementation
344        Ok(Vec::new())
345    }
346}
347
348/// Property path optimizer for query planning
349pub struct PropertyPathOptimizer {
350    /// Enable path rewriting
351    rewrite_enabled: bool,
352    /// Enable path decomposition
353    decompose_enabled: bool,
354}
355
356impl Default for PropertyPathOptimizer {
357    fn default() -> Self {
358        Self::new()
359    }
360}
361
362impl PropertyPathOptimizer {
363    /// Create new optimizer
364    pub fn new() -> Self {
365        PropertyPathOptimizer {
366            rewrite_enabled: true,
367            decompose_enabled: true,
368        }
369    }
370
371    /// Optimize a property path
372    pub fn optimize(&self, path: PropertyPath) -> PropertyPath {
373        if !self.rewrite_enabled {
374            return path;
375        }
376
377        // Apply optimization rules
378        self.optimize_recursive(path)
379    }
380
381    #[allow(clippy::only_used_in_recursion)]
382    fn optimize_recursive(&self, path: PropertyPath) -> PropertyPath {
383        match path {
384            // Optimize p/p to p{2}
385            PropertyPath::Sequence(ref l, ref r) if l == r => {
386                PropertyPath::FixedLength(l.clone(), 2)
387            }
388
389            // Optimize p? | p+ to p*
390            PropertyPath::Alternative(ref l, ref r) => match (l.as_ref(), r.as_ref()) {
391                (PropertyPath::ZeroOrOne(p1), PropertyPath::OneOrMore(p2)) if p1 == p2 => {
392                    PropertyPath::ZeroOrMore(p1.clone())
393                }
394                (PropertyPath::OneOrMore(p1), PropertyPath::ZeroOrOne(p2)) if p1 == p2 => {
395                    PropertyPath::ZeroOrMore(p1.clone())
396                }
397                _ => PropertyPath::Alternative(
398                    Box::new(self.optimize_recursive(*l.clone())),
399                    Box::new(self.optimize_recursive(*r.clone())),
400                ),
401            },
402
403            // Recursively optimize nested paths
404            PropertyPath::Inverse(p) => {
405                PropertyPath::Inverse(Box::new(self.optimize_recursive(*p)))
406            }
407            PropertyPath::Sequence(l, r) => PropertyPath::Sequence(
408                Box::new(self.optimize_recursive(*l)),
409                Box::new(self.optimize_recursive(*r)),
410            ),
411            PropertyPath::ZeroOrMore(p) => {
412                PropertyPath::ZeroOrMore(Box::new(self.optimize_recursive(*p)))
413            }
414            PropertyPath::OneOrMore(p) => {
415                PropertyPath::OneOrMore(Box::new(self.optimize_recursive(*p)))
416            }
417            PropertyPath::ZeroOrOne(p) => {
418                PropertyPath::ZeroOrOne(Box::new(self.optimize_recursive(*p)))
419            }
420            PropertyPath::FixedLength(p, n) => {
421                PropertyPath::FixedLength(Box::new(self.optimize_recursive(*p)), n)
422            }
423            PropertyPath::RangeLength(p, min, max) => {
424                PropertyPath::RangeLength(Box::new(self.optimize_recursive(*p)), min, max)
425            }
426            PropertyPath::Distinct(p) => {
427                PropertyPath::Distinct(Box::new(self.optimize_recursive(*p)))
428            }
429
430            // Base cases
431            PropertyPath::Predicate(_) | PropertyPath::NegatedPropertySet(_) => path,
432        }
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_property_path_creation() {
442        let p1 = NamedNode::new("http://example.org/knows").expect("valid IRI");
443        let p2 = NamedNode::new("http://example.org/likes").expect("valid IRI");
444
445        // Simple predicate
446        let path = PropertyPath::predicate(p1.clone());
447        assert_eq!(path.min_length(), 1);
448        assert_eq!(path.max_length(), Some(1));
449
450        // Sequence
451        let seq = PropertyPath::sequence(
452            PropertyPath::predicate(p1.clone()),
453            PropertyPath::predicate(p2.clone()),
454        );
455        assert_eq!(seq.min_length(), 2);
456        assert_eq!(seq.max_length(), Some(2));
457
458        // Zero or more
459        let star = PropertyPath::zero_or_more(PropertyPath::predicate(p1.clone()));
460        assert_eq!(star.min_length(), 0);
461        assert_eq!(star.max_length(), None);
462
463        // Fixed length
464        let fixed = PropertyPath::fixed_length(PropertyPath::predicate(p1.clone()), 3);
465        assert_eq!(fixed.min_length(), 3);
466        assert_eq!(fixed.max_length(), Some(3));
467    }
468
469    #[test]
470    fn test_property_path_display() {
471        let p1 = NamedNode::new("http://example.org/p").expect("valid IRI");
472        let p2 = NamedNode::new("http://example.org/q").expect("valid IRI");
473
474        let path = PropertyPath::sequence(
475            PropertyPath::predicate(p1.clone()),
476            PropertyPath::zero_or_more(PropertyPath::predicate(p2.clone())),
477        );
478
479        let expected = format!("{p1}/{p2}*");
480        assert_eq!(format!("{path}"), expected);
481    }
482
483    #[test]
484    fn test_path_optimization() {
485        let optimizer = PropertyPathOptimizer::new();
486        let p = PropertyPath::predicate(NamedNode::new("http://example.org/p").expect("valid IRI"));
487
488        // Optimize p/p to p{2}
489        let seq = PropertyPath::sequence(p.clone(), p.clone());
490        let optimized = optimizer.optimize(seq);
491        assert!(matches!(optimized, PropertyPath::FixedLength(_, 2)));
492
493        // Optimize p? | p+ to p*
494        let alt = PropertyPath::alternative(
495            PropertyPath::zero_or_one(p.clone()),
496            PropertyPath::one_or_more(p.clone()),
497        );
498        let optimized = optimizer.optimize(alt);
499        assert!(matches!(optimized, PropertyPath::ZeroOrMore(_)));
500    }
501}