Skip to main content

oxirs_core/model/
pattern.rs

1//! Pattern matching for RDF triples
2//!
3//! This module provides pattern matching functionality for querying RDF triples.
4
5use crate::model::{BlankNode, Literal, NamedNode, Object, Predicate, Subject, Triple, Variable};
6
7/// A pattern for matching triples
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct TriplePattern {
11    pub subject: Option<SubjectPattern>,
12    pub predicate: Option<PredicatePattern>,
13    pub object: Option<ObjectPattern>,
14}
15
16impl TriplePattern {
17    /// Create a new triple pattern
18    pub fn new(
19        subject: Option<SubjectPattern>,
20        predicate: Option<PredicatePattern>,
21        object: Option<ObjectPattern>,
22    ) -> Self {
23        TriplePattern {
24            subject,
25            predicate,
26            object,
27        }
28    }
29
30    /// Get the subject pattern
31    pub fn subject(&self) -> Option<&SubjectPattern> {
32        self.subject.as_ref()
33    }
34
35    /// Get the predicate pattern
36    pub fn predicate(&self) -> Option<&PredicatePattern> {
37        self.predicate.as_ref()
38    }
39
40    /// Get the object pattern
41    pub fn object(&self) -> Option<&ObjectPattern> {
42        self.object.as_ref()
43    }
44
45    /// Check if a triple matches this pattern
46    pub fn matches(&self, triple: &Triple) -> bool {
47        // Check subject
48        if let Some(ref subject_pattern) = self.subject {
49            if !subject_pattern.matches(triple.subject()) {
50                return false;
51            }
52        }
53
54        // Check predicate
55        if let Some(ref predicate_pattern) = self.predicate {
56            if !predicate_pattern.matches(triple.predicate()) {
57                return false;
58            }
59        }
60
61        // Check object
62        if let Some(ref object_pattern) = self.object {
63            if !object_pattern.matches(triple.object()) {
64                return false;
65            }
66        }
67
68        true
69    }
70}
71
72/// Pattern for matching subjects
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub enum SubjectPattern {
76    NamedNode(NamedNode),
77    BlankNode(BlankNode),
78    Variable(Variable),
79    /// RDF-star: a quoted triple used as a subject
80    QuotedTriple(Box<crate::query::algebra::AlgebraTriplePattern>),
81}
82
83impl SubjectPattern {
84    /// Get the string representation
85    pub fn as_str(&self) -> &str {
86        match self {
87            SubjectPattern::NamedNode(nn) => nn.as_str(),
88            SubjectPattern::BlankNode(bn) => bn.as_str(),
89            SubjectPattern::Variable(v) => v.as_str(),
90            SubjectPattern::QuotedTriple(_) => "<<quoted-triple>>",
91        }
92    }
93
94    fn matches(&self, subject: &Subject) -> bool {
95        match (self, subject) {
96            (SubjectPattern::NamedNode(pn), Subject::NamedNode(sn)) => pn == sn,
97            (SubjectPattern::BlankNode(pb), Subject::BlankNode(sb)) => pb == sb,
98            (SubjectPattern::Variable(_), _) => true,
99            // A quoted-triple pattern matches a quoted-triple subject only when the
100            // inner pattern structurally matches the inner triple (variables act as
101            // wildcards). A fully-ground inner pattern requires exact equality.
102            (SubjectPattern::QuotedTriple(pat), Subject::QuotedTriple(sqt)) => {
103                algebra_pattern_matches_triple(pat, sqt.inner())
104            }
105            _ => false,
106        }
107    }
108}
109
110/// Structurally match a SPARQL algebra triple pattern against a concrete triple.
111///
112/// Variables in the pattern act as wildcards; concrete terms (including nested
113/// quoted triples) must match exactly. This is used for RDF-star quoted-triple
114/// pattern matching so that a ground quoted-triple pattern such as
115/// `<< <a> <b> <c> >> ?p ?o` does NOT spuriously match a triple whose subject is
116/// `<< <x> <y> <z> >>`.
117pub(crate) fn algebra_pattern_matches_triple(
118    pattern: &crate::query::algebra::AlgebraTriplePattern,
119    triple: &Triple,
120) -> bool {
121    term_pattern_matches_subject(&pattern.subject, triple.subject())
122        && term_pattern_matches_predicate(&pattern.predicate, triple.predicate())
123        && term_pattern_matches_object(&pattern.object, triple.object())
124}
125
126fn term_pattern_matches_subject(
127    pattern: &crate::query::algebra::TermPattern,
128    subject: &Subject,
129) -> bool {
130    use crate::query::algebra::TermPattern;
131    match (pattern, subject) {
132        (TermPattern::Variable(_), _) => true,
133        (TermPattern::NamedNode(pn), Subject::NamedNode(sn)) => pn == sn,
134        (TermPattern::BlankNode(pb), Subject::BlankNode(sb)) => pb == sb,
135        (TermPattern::QuotedTriple(pat), Subject::QuotedTriple(sqt)) => {
136            algebra_pattern_matches_triple(pat, sqt.inner())
137        }
138        _ => false,
139    }
140}
141
142fn term_pattern_matches_predicate(
143    pattern: &crate::query::algebra::TermPattern,
144    predicate: &Predicate,
145) -> bool {
146    use crate::query::algebra::TermPattern;
147    match (pattern, predicate) {
148        (TermPattern::Variable(_), _) => true,
149        (TermPattern::NamedNode(pn), Predicate::NamedNode(sn)) => pn == sn,
150        _ => false,
151    }
152}
153
154fn term_pattern_matches_object(
155    pattern: &crate::query::algebra::TermPattern,
156    object: &Object,
157) -> bool {
158    use crate::query::algebra::TermPattern;
159    match (pattern, object) {
160        (TermPattern::Variable(_), _) => true,
161        (TermPattern::NamedNode(pn), Object::NamedNode(on)) => pn == on,
162        (TermPattern::BlankNode(pb), Object::BlankNode(ob)) => pb == ob,
163        (TermPattern::Literal(pl), Object::Literal(ol)) => pl == ol,
164        (TermPattern::QuotedTriple(pat), Object::QuotedTriple(oqt)) => {
165            algebra_pattern_matches_triple(pat, oqt.inner())
166        }
167        _ => false,
168    }
169}
170
171/// Pattern for matching predicates
172#[derive(Debug, Clone, PartialEq, Eq, Hash)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub enum PredicatePattern {
175    NamedNode(NamedNode),
176    Variable(Variable),
177}
178
179impl PredicatePattern {
180    /// Get the string representation
181    pub fn as_str(&self) -> &str {
182        match self {
183            PredicatePattern::NamedNode(nn) => nn.as_str(),
184            PredicatePattern::Variable(v) => v.as_str(),
185        }
186    }
187
188    fn matches(&self, predicate: &Predicate) -> bool {
189        match (self, predicate) {
190            (PredicatePattern::NamedNode(pn), Predicate::NamedNode(sn)) => pn == sn,
191            (PredicatePattern::Variable(_), _) => true,
192            _ => false,
193        }
194    }
195}
196
197/// Pattern for matching objects
198#[derive(Debug, Clone, PartialEq, Eq, Hash)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub enum ObjectPattern {
201    NamedNode(NamedNode),
202    BlankNode(BlankNode),
203    Literal(Literal),
204    Variable(Variable),
205    /// RDF-star: a quoted triple used as an object
206    QuotedTriple(Box<crate::query::algebra::AlgebraTriplePattern>),
207}
208
209impl ObjectPattern {
210    /// Get the string representation
211    pub fn as_str(&self) -> &str {
212        match self {
213            ObjectPattern::NamedNode(nn) => nn.as_str(),
214            ObjectPattern::BlankNode(bn) => bn.as_str(),
215            ObjectPattern::Literal(l) => l.value(),
216            ObjectPattern::Variable(v) => v.as_str(),
217            ObjectPattern::QuotedTriple(_) => "<<quoted-triple>>",
218        }
219    }
220
221    fn matches(&self, object: &Object) -> bool {
222        match (self, object) {
223            (ObjectPattern::NamedNode(pn), Object::NamedNode(on)) => pn == on,
224            (ObjectPattern::BlankNode(pb), Object::BlankNode(ob)) => pb == ob,
225            (ObjectPattern::Literal(pl), Object::Literal(ol)) => pl == ol,
226            (ObjectPattern::Variable(_), _) => true,
227            // A quoted-triple pattern matches a quoted-triple object only when the
228            // inner pattern structurally matches the inner triple (variables act as
229            // wildcards). A fully-ground inner pattern requires exact equality.
230            (ObjectPattern::QuotedTriple(pat), Object::QuotedTriple(oqt)) => {
231                algebra_pattern_matches_triple(pat, oqt.inner())
232            }
233            _ => false,
234        }
235    }
236}
237
238// TryFrom implementations for converting TermPattern to positional patterns.
239// Using TryFrom rather than From because certain TermPattern variants are semantically
240// invalid for particular positions (e.g., Literal cannot be a subject in RDF).
241use crate::query::algebra::TermPattern;
242
243/// Error type for invalid TermPattern-to-positional-pattern conversions.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct InvalidPatternConversion {
246    pub reason: &'static str,
247}
248
249impl std::fmt::Display for InvalidPatternConversion {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        write!(f, "Invalid pattern conversion: {}", self.reason)
252    }
253}
254
255impl std::error::Error for InvalidPatternConversion {}
256
257impl TryFrom<TermPattern> for SubjectPattern {
258    type Error = InvalidPatternConversion;
259
260    fn try_from(term: TermPattern) -> Result<Self, Self::Error> {
261        match term {
262            TermPattern::NamedNode(n) => Ok(SubjectPattern::NamedNode(n)),
263            TermPattern::BlankNode(b) => Ok(SubjectPattern::BlankNode(b)),
264            TermPattern::Variable(v) => Ok(SubjectPattern::Variable(v)),
265            TermPattern::QuotedTriple(qt) => Ok(SubjectPattern::QuotedTriple(qt)),
266            TermPattern::Literal(_) => Err(InvalidPatternConversion {
267                reason: "Literals cannot be used as subjects in RDF",
268            }),
269        }
270    }
271}
272
273impl TryFrom<TermPattern> for PredicatePattern {
274    type Error = InvalidPatternConversion;
275
276    fn try_from(term: TermPattern) -> Result<Self, Self::Error> {
277        match term {
278            TermPattern::NamedNode(n) => Ok(PredicatePattern::NamedNode(n)),
279            TermPattern::Variable(v) => Ok(PredicatePattern::Variable(v)),
280            TermPattern::BlankNode(_) => Err(InvalidPatternConversion {
281                reason: "Blank nodes cannot be used as predicates in RDF",
282            }),
283            TermPattern::Literal(_) => Err(InvalidPatternConversion {
284                reason: "Literals cannot be used as predicates in RDF",
285            }),
286            TermPattern::QuotedTriple(_) => Err(InvalidPatternConversion {
287                reason: "Quoted triples cannot be used as predicates in RDF",
288            }),
289        }
290    }
291}
292
293impl TryFrom<TermPattern> for ObjectPattern {
294    type Error = InvalidPatternConversion;
295
296    fn try_from(term: TermPattern) -> Result<Self, Self::Error> {
297        match term {
298            TermPattern::NamedNode(n) => Ok(ObjectPattern::NamedNode(n)),
299            TermPattern::BlankNode(b) => Ok(ObjectPattern::BlankNode(b)),
300            TermPattern::Literal(l) => Ok(ObjectPattern::Literal(l)),
301            TermPattern::Variable(v) => Ok(ObjectPattern::Variable(v)),
302            TermPattern::QuotedTriple(qt) => Ok(ObjectPattern::QuotedTriple(qt)),
303        }
304    }
305}
306
307// TryFrom implementations for converting patterns to concrete terms
308impl TryFrom<&SubjectPattern> for Subject {
309    type Error = ();
310
311    fn try_from(pattern: &SubjectPattern) -> Result<Self, Self::Error> {
312        match pattern {
313            SubjectPattern::NamedNode(n) => Ok(Subject::NamedNode(n.clone())),
314            SubjectPattern::BlankNode(b) => Ok(Subject::BlankNode(b.clone())),
315            SubjectPattern::Variable(_) => Err(()),
316            SubjectPattern::QuotedTriple(qt) => {
317                // Convert AlgebraTriplePattern to a concrete QuotedTriple if possible.
318                // This requires all sub-terms to be ground (non-variable).
319                use crate::model::star::QuotedTriple;
320                let inner_subj = match &qt.subject {
321                    TermPattern::NamedNode(n) => Subject::NamedNode(n.clone()),
322                    TermPattern::BlankNode(b) => Subject::BlankNode(b.clone()),
323                    _ => return Err(()),
324                };
325                let inner_pred = match &qt.predicate {
326                    TermPattern::NamedNode(n) => Predicate::NamedNode(n.clone()),
327                    _ => return Err(()),
328                };
329                let inner_obj = match &qt.object {
330                    TermPattern::NamedNode(n) => Object::NamedNode(n.clone()),
331                    TermPattern::BlankNode(b) => Object::BlankNode(b.clone()),
332                    TermPattern::Literal(l) => Object::Literal(l.clone()),
333                    _ => return Err(()),
334                };
335                Ok(Subject::QuotedTriple(Box::new(QuotedTriple::new(
336                    Triple::new(inner_subj, inner_pred, inner_obj),
337                ))))
338            }
339        }
340    }
341}
342
343impl TryFrom<&PredicatePattern> for Predicate {
344    type Error = ();
345
346    fn try_from(pattern: &PredicatePattern) -> Result<Self, Self::Error> {
347        match pattern {
348            PredicatePattern::NamedNode(n) => Ok(Predicate::NamedNode(n.clone())),
349            PredicatePattern::Variable(_) => Err(()),
350        }
351    }
352}
353
354impl TryFrom<&ObjectPattern> for Object {
355    type Error = ();
356
357    fn try_from(pattern: &ObjectPattern) -> Result<Self, Self::Error> {
358        match pattern {
359            ObjectPattern::NamedNode(n) => Ok(Object::NamedNode(n.clone())),
360            ObjectPattern::BlankNode(b) => Ok(Object::BlankNode(b.clone())),
361            ObjectPattern::Literal(l) => Ok(Object::Literal(l.clone())),
362            ObjectPattern::Variable(_) => Err(()),
363            ObjectPattern::QuotedTriple(qt) => {
364                // Convert AlgebraTriplePattern to a concrete QuotedTriple if all terms are ground.
365                use crate::model::star::QuotedTriple;
366                let inner_subj = match &qt.subject {
367                    TermPattern::NamedNode(n) => Subject::NamedNode(n.clone()),
368                    TermPattern::BlankNode(b) => Subject::BlankNode(b.clone()),
369                    _ => return Err(()),
370                };
371                let inner_pred = match &qt.predicate {
372                    TermPattern::NamedNode(n) => Predicate::NamedNode(n.clone()),
373                    _ => return Err(()),
374                };
375                let inner_obj = match &qt.object {
376                    TermPattern::NamedNode(n) => Object::NamedNode(n.clone()),
377                    TermPattern::BlankNode(b) => Object::BlankNode(b.clone()),
378                    TermPattern::Literal(l) => Object::Literal(l.clone()),
379                    _ => return Err(()),
380                };
381                Ok(Object::QuotedTriple(Box::new(QuotedTriple::new(
382                    Triple::new(inner_subj, inner_pred, inner_obj),
383                ))))
384            }
385        }
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_pattern_matching() {
395        let subject = NamedNode::new("http://example.org/s").expect("valid IRI");
396        let predicate = NamedNode::new("http://example.org/p").expect("valid IRI");
397        let object = Literal::new("o");
398
399        let triple = Triple::new(subject.clone(), predicate.clone(), object.clone());
400
401        // Test exact match
402        let pattern = TriplePattern::new(
403            Some(SubjectPattern::NamedNode(subject.clone())),
404            Some(PredicatePattern::NamedNode(predicate.clone())),
405            Some(ObjectPattern::Literal(object.clone())),
406        );
407        assert!(pattern.matches(&triple));
408
409        // Test wildcard match
410        let pattern = TriplePattern::new(None, None, None);
411        assert!(pattern.matches(&triple));
412
413        // Test variable match
414        let pattern = TriplePattern::new(
415            Some(SubjectPattern::Variable(
416                Variable::new("s").expect("valid variable name"),
417            )),
418            Some(PredicatePattern::Variable(
419                Variable::new("p").expect("valid variable name"),
420            )),
421            Some(ObjectPattern::Variable(
422                Variable::new("o").expect("valid variable name"),
423            )),
424        );
425        assert!(pattern.matches(&triple));
426
427        // Test non-match
428        let different_subject = NamedNode::new("http://example.org/different").expect("valid IRI");
429        let pattern = TriplePattern::new(
430            Some(SubjectPattern::NamedNode(different_subject)),
431            None,
432            None,
433        );
434        assert!(!pattern.matches(&triple));
435    }
436
437    #[test]
438    fn regression_ground_quoted_triple_pattern_is_exact() {
439        use crate::model::star::QuotedTriple;
440        use crate::query::algebra::{AlgebraTriplePattern, TermPattern};
441
442        let nn = |s: &str| NamedNode::new(s).expect("valid IRI");
443
444        // Inner triple stored in the data: << <a> <b> <c> >>
445        let stored_inner = Triple::new(nn("http://ex/a"), nn("http://ex/b"), nn("http://ex/c"));
446        let subject = Subject::QuotedTriple(Box::new(QuotedTriple::new(stored_inner.clone())));
447
448        // Ground pattern << <a> <b> <c> >> must match the identical inner triple.
449        let ground_match = AlgebraTriplePattern::new(
450            TermPattern::NamedNode(nn("http://ex/a")),
451            TermPattern::NamedNode(nn("http://ex/b")),
452            TermPattern::NamedNode(nn("http://ex/c")),
453        );
454        let sp_match = SubjectPattern::QuotedTriple(Box::new(ground_match));
455        assert!(sp_match.matches(&subject));
456
457        // Ground pattern << <x> <y> <z> >> must NOT match (previously returned true).
458        let ground_diff = AlgebraTriplePattern::new(
459            TermPattern::NamedNode(nn("http://ex/x")),
460            TermPattern::NamedNode(nn("http://ex/y")),
461            TermPattern::NamedNode(nn("http://ex/z")),
462        );
463        let sp_diff = SubjectPattern::QuotedTriple(Box::new(ground_diff));
464        assert!(!sp_diff.matches(&subject));
465
466        // A pattern with variable positions acts as a wildcard and matches.
467        let var_pat = AlgebraTriplePattern::new(
468            TermPattern::NamedNode(nn("http://ex/a")),
469            TermPattern::Variable(Variable::new("p").expect("valid var")),
470            TermPattern::Variable(Variable::new("o").expect("valid var")),
471        );
472        let sp_var = SubjectPattern::QuotedTriple(Box::new(var_pat));
473        assert!(sp_var.matches(&subject));
474
475        // Same semantics for the object position.
476        let object = Object::QuotedTriple(Box::new(QuotedTriple::new(stored_inner)));
477        let op_diff = ObjectPattern::QuotedTriple(Box::new(AlgebraTriplePattern::new(
478            TermPattern::NamedNode(nn("http://ex/x")),
479            TermPattern::NamedNode(nn("http://ex/y")),
480            TermPattern::NamedNode(nn("http://ex/z")),
481        )));
482        assert!(!op_diff.matches(&object));
483    }
484}