use oxirs_core::model::Triple;
use oxirs_core::RdfTerm;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TriplePattern {
subject: Option<String>,
predicate: Option<String>,
object: Option<String>,
}
impl TriplePattern {
pub fn new() -> Self {
Self {
subject: None,
predicate: None,
object: None,
}
}
pub fn with_subject(mut self, subject: &str) -> Self {
self.subject = Some(subject.to_string());
self
}
pub fn with_predicate(mut self, predicate: &str) -> Self {
self.predicate = Some(predicate.to_string());
self
}
pub fn with_object(mut self, object: &str) -> Self {
self.object = Some(object.to_string());
self
}
pub fn matches(&self, triple: &Triple) -> bool {
if let Some(ref subject) = self.subject {
if !triple.subject().as_str().contains(subject) {
return false;
}
}
if let Some(ref predicate) = self.predicate {
if !triple.predicate().as_str().contains(predicate) {
return false;
}
}
if let Some(ref object) = self.object {
if !triple.object().as_str().contains(object) {
return false;
}
}
true
}
pub fn is_empty(&self) -> bool {
self.subject.is_none() && self.predicate.is_none() && self.object.is_none()
}
}
impl Default for TriplePattern {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct PatternMatcher<'a> {
triples: &'a [Triple],
subject_index: HashMap<String, Vec<usize>>,
predicate_index: HashMap<String, Vec<usize>>,
object_index: HashMap<String, Vec<usize>>,
}
impl<'a> PatternMatcher<'a> {
pub fn new(triples: &'a [Triple]) -> Self {
let mut subject_index: HashMap<String, Vec<usize>> = HashMap::new();
let mut predicate_index: HashMap<String, Vec<usize>> = HashMap::new();
let mut object_index: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, triple) in triples.iter().enumerate() {
subject_index
.entry(triple.subject().to_string())
.or_default()
.push(idx);
predicate_index
.entry(triple.predicate().to_string())
.or_default()
.push(idx);
object_index
.entry(triple.object().to_string())
.or_default()
.push(idx);
}
Self {
triples,
subject_index,
predicate_index,
object_index,
}
}
pub fn find_matches(&self, pattern: &TriplePattern) -> Vec<&Triple> {
if pattern.is_empty() {
return self.triples.iter().collect();
}
let candidates = if let Some(ref subject) = pattern.subject {
self.subject_index
.iter()
.filter(|(k, _)| k.contains(subject))
.flat_map(|(_, indices)| indices.iter().copied())
.collect::<Vec<_>>()
} else if let Some(ref predicate) = pattern.predicate {
self.predicate_index
.iter()
.filter(|(k, _)| k.contains(predicate))
.flat_map(|(_, indices)| indices.iter().copied())
.collect::<Vec<_>>()
} else if let Some(ref object) = pattern.object {
self.object_index
.iter()
.filter(|(k, _)| k.contains(object))
.flat_map(|(_, indices)| indices.iter().copied())
.collect::<Vec<_>>()
} else {
(0..self.triples.len()).collect()
};
candidates
.into_iter()
.filter_map(|idx| {
let triple = &self.triples[idx];
if pattern.matches(triple) {
Some(triple)
} else {
None
}
})
.collect()
}
pub fn count_matches(&self, pattern: &TriplePattern) -> usize {
if pattern.is_empty() {
return self.triples.len();
}
self.triples.iter().filter(|t| pattern.matches(t)).count()
}
pub fn has_match(&self, pattern: &TriplePattern) -> bool {
if pattern.is_empty() {
return !self.triples.is_empty();
}
self.triples.iter().any(|t| pattern.matches(t))
}
pub fn subjects(&self) -> Vec<String> {
self.subject_index.keys().cloned().collect()
}
pub fn predicates(&self) -> Vec<String> {
self.predicate_index.keys().cloned().collect()
}
pub fn objects(&self) -> Vec<String> {
self.object_index.keys().cloned().collect()
}
}
#[derive(Debug)]
pub struct QueryBuilder {
patterns: Vec<TriplePattern>,
limit: Option<usize>,
offset: usize,
}
impl QueryBuilder {
pub fn new() -> Self {
Self {
patterns: Vec::new(),
limit: None,
offset: 0,
}
}
pub fn pattern(mut self, pattern: TriplePattern) -> Self {
self.patterns.push(pattern);
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
pub fn execute<'a>(&self, matcher: &'a PatternMatcher) -> Vec<&'a Triple> {
if self.patterns.is_empty() {
return Vec::new();
}
let mut results = matcher.find_matches(&self.patterns[0]);
for pattern in &self.patterns[1..] {
let pattern_results = matcher.find_matches(pattern);
results.retain(|t| pattern_results.contains(t));
}
let results: Vec<_> = results.into_iter().skip(self.offset).collect();
if let Some(limit) = self.limit {
results.into_iter().take(limit).collect()
} else {
results
}
}
pub fn count(&self, matcher: &PatternMatcher) -> usize {
if self.patterns.is_empty() {
return 0;
}
let mut results = matcher.find_matches(&self.patterns[0]);
for pattern in &self.patterns[1..] {
let pattern_results = matcher.find_matches(pattern);
results.retain(|t| pattern_results.contains(t));
}
if self.offset >= results.len() {
return 0;
}
let remaining = results.len() - self.offset;
self.limit.map_or(remaining, |limit| remaining.min(limit))
}
}
impl Default for QueryBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxirs_core::model::NamedNode;
fn create_test_triple(s: &str, p: &str, o: &str) -> Triple {
Triple::new(
NamedNode::new(s).expect("valid IRI"),
NamedNode::new(p).expect("valid IRI"),
NamedNode::new(o).expect("valid IRI"),
)
}
#[test]
fn test_pattern_matching() {
let triples = vec![
create_test_triple(
"http://example.org/alice",
"http://xmlns.com/foaf/0.1/knows",
"http://example.org/bob",
),
create_test_triple(
"http://example.org/bob",
"http://xmlns.com/foaf/0.1/knows",
"http://example.org/charlie",
),
create_test_triple(
"http://example.org/alice",
"http://example.org/age",
"http://example.org/30",
),
];
let matcher = PatternMatcher::new(&triples);
let pattern = TriplePattern::new().with_predicate("foaf/0.1/knows");
let results = matcher.find_matches(&pattern);
assert_eq!(results.len(), 2);
let pattern = TriplePattern::new().with_subject("alice");
let results = matcher.find_matches(&pattern);
assert_eq!(results.len(), 2);
let pattern = TriplePattern::new()
.with_subject("alice")
.with_predicate("knows");
let results = matcher.find_matches(&pattern);
assert_eq!(results.len(), 1);
}
#[test]
fn test_count_and_has_match() {
let triples = vec![create_test_triple(
"http://example.org/s",
"http://example.org/p",
"http://example.org/o",
)];
let matcher = PatternMatcher::new(&triples);
let pattern = TriplePattern::new().with_subject("example.org/s");
assert_eq!(matcher.count_matches(&pattern), 1);
assert!(matcher.has_match(&pattern));
let no_match = TriplePattern::new().with_subject("notfound");
assert!(!matcher.has_match(&no_match));
}
#[test]
fn test_query_builder() {
let triples = vec![
create_test_triple(
"http://example.org/alice",
"http://xmlns.com/foaf/0.1/knows",
"http://example.org/bob",
),
create_test_triple(
"http://example.org/bob",
"http://xmlns.com/foaf/0.1/knows",
"http://example.org/charlie",
),
create_test_triple(
"http://example.org/alice",
"http://example.org/age",
"http://example.org/30",
),
];
let matcher = PatternMatcher::new(&triples);
let query = QueryBuilder::new()
.pattern(TriplePattern::new().with_predicate("knows"))
.limit(1);
let results = query.execute(&matcher);
assert_eq!(results.len(), 1);
let count = query.count(&matcher);
assert_eq!(count, 1);
}
#[test]
fn test_empty_pattern() {
let triples = vec![create_test_triple(
"http://example.org/s",
"http://example.org/p",
"http://example.org/o",
)];
let matcher = PatternMatcher::new(&triples);
let pattern = TriplePattern::new();
assert!(pattern.is_empty());
assert_eq!(matcher.find_matches(&pattern).len(), 1);
}
#[test]
fn test_index_queries() {
let triples = vec![
create_test_triple(
"http://example.org/s1",
"http://example.org/p",
"http://example.org/o1",
),
create_test_triple(
"http://example.org/s2",
"http://example.org/p",
"http://example.org/o2",
),
];
let matcher = PatternMatcher::new(&triples);
let subjects = matcher.subjects();
assert_eq!(subjects.len(), 2);
let predicates = matcher.predicates();
assert_eq!(predicates.len(), 1);
let objects = matcher.objects();
assert_eq!(objects.len(), 2);
}
#[test]
fn test_query_with_offset() {
let triples = vec![
create_test_triple(
"http://example.org/s1",
"http://example.org/p",
"http://example.org/o1",
),
create_test_triple(
"http://example.org/s2",
"http://example.org/p",
"http://example.org/o2",
),
create_test_triple(
"http://example.org/s3",
"http://example.org/p",
"http://example.org/o3",
),
];
let matcher = PatternMatcher::new(&triples);
let query = QueryBuilder::new()
.pattern(TriplePattern::new().with_predicate("example.org/p"))
.offset(1)
.limit(1);
let results = query.execute(&matcher);
assert_eq!(results.len(), 1);
}
}