Skip to main content

aingle_graph/
query.rs

1// Copyright 2019-2026 Apilium Technologies OÜ. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR Commercial
3
4//! Query engine for the graph database.
5//!
6//! This module provides a `QueryBuilder` for pattern matching and a `TraversalBuilder`
7//! for graph traversal.
8
9use crate::{GraphStore, NodeId, Predicate, Result, Triple, Value};
10
11/// A pattern for matching `(Subject, Predicate, Object)` triples.
12///
13/// A pattern specifies constraints on the components of a triple. Any component
14/// can be `None`, which acts as a wildcard that matches any value.
15///
16/// # Examples
17///
18/// Match all triples with a specific subject:
19///
20/// ```
21/// use aingle_graph::{TriplePattern, NodeId};
22///
23/// let pattern = TriplePattern::subject(NodeId::named("user:alice"));
24/// ```
25///
26/// Match triples with specific subject and predicate:
27///
28/// ```
29/// use aingle_graph::{TriplePattern, NodeId, Predicate};
30///
31/// let pattern = TriplePattern::subject(NodeId::named("user:alice"))
32///     .with_predicate(Predicate::named("has_name"));
33/// ```
34///
35/// Match all triples (wildcard pattern):
36///
37/// ```
38/// use aingle_graph::TriplePattern;
39///
40/// let pattern = TriplePattern::any();
41/// assert!(pattern.is_wildcard());
42/// ```
43#[derive(Debug, Clone, Default)]
44pub struct TriplePattern {
45    /// An optional constraint on the triple's subject.
46    pub subject: Option<NodeId>,
47    /// An optional constraint on the triple's predicate.
48    pub predicate: Option<Predicate>,
49    /// An optional constraint on the triple's object.
50    pub object: Option<Value>,
51}
52
53impl TriplePattern {
54    /// Creates a new pattern that matches any triple.
55    ///
56    /// This is equivalent to a wildcard pattern with no constraints.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use aingle_graph::TriplePattern;
62    ///
63    /// let pattern = TriplePattern::any();
64    /// assert!(pattern.is_wildcard());
65    /// ```
66    pub fn any() -> Self {
67        Self::default()
68    }
69
70    /// Creates a new pattern that matches a specific subject.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use aingle_graph::{TriplePattern, NodeId};
76    ///
77    /// let pattern = TriplePattern::subject(NodeId::named("user:alice"));
78    /// ```
79    pub fn subject(subject: NodeId) -> Self {
80        Self {
81            subject: Some(subject),
82            ..Default::default()
83        }
84    }
85
86    /// Creates a new pattern that matches a specific predicate.
87    pub fn predicate(predicate: Predicate) -> Self {
88        Self {
89            predicate: Some(predicate),
90            ..Default::default()
91        }
92    }
93
94    /// Creates a new pattern that matches a specific object.
95    pub fn object(object: Value) -> Self {
96        Self {
97            object: Some(object),
98            ..Default::default()
99        }
100    }
101
102    /// Adds a subject constraint to the pattern.
103    pub fn with_subject(mut self, subject: NodeId) -> Self {
104        self.subject = Some(subject);
105        self
106    }
107
108    /// Adds a predicate constraint to the pattern.
109    pub fn with_predicate(mut self, predicate: Predicate) -> Self {
110        self.predicate = Some(predicate);
111        self
112    }
113
114    /// Adds an object constraint to the pattern.
115    pub fn with_object(mut self, object: Value) -> Self {
116        self.object = Some(object);
117        self
118    }
119
120    /// Returns `true` if the given [`Triple`] matches this pattern.
121    ///
122    /// A triple matches the pattern if all non-None constraints are satisfied.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use aingle_graph::{Triple, TriplePattern, NodeId, Predicate, Value};
128    ///
129    /// let triple = Triple::new(
130    ///     NodeId::named("user:alice"),
131    ///     Predicate::named("has_name"),
132    ///     Value::literal("Alice"),
133    /// );
134    ///
135    /// let pattern = TriplePattern::subject(NodeId::named("user:alice"));
136    /// assert!(pattern.matches(&triple));
137    ///
138    /// let wrong_pattern = TriplePattern::subject(NodeId::named("user:bob"));
139    /// assert!(!wrong_pattern.matches(&triple));
140    /// ```
141    pub fn matches(&self, triple: &Triple) -> bool {
142        if let Some(ref s) = self.subject {
143            if &triple.subject != s {
144                return false;
145            }
146        }
147        if let Some(ref p) = self.predicate {
148            if &triple.predicate != p {
149                return false;
150            }
151        }
152        if let Some(ref o) = self.object {
153            if &triple.object != o {
154                return false;
155            }
156        }
157        true
158    }
159
160    /// Returns `true` if all components (subject, predicate, object) of the pattern are specified.
161    pub fn is_exact(&self) -> bool {
162        self.subject.is_some() && self.predicate.is_some() && self.object.is_some()
163    }
164
165    /// Returns `true` if the pattern is a wildcard (all components are `None`).
166    pub fn is_wildcard(&self) -> bool {
167        self.subject.is_none() && self.predicate.is_none() && self.object.is_none()
168    }
169}
170
171/// The result of a query execution.
172///
173/// Contains the matched triples along with metadata about the result set,
174/// including the total count and whether there are more results available.
175///
176/// # Examples
177///
178/// ```
179/// use aingle_graph::{GraphDB, Triple, NodeId, Predicate, Value};
180///
181/// # fn main() -> Result<(), aingle_graph::Error> {
182/// let db = GraphDB::memory()?;
183///
184/// db.insert(Triple::new(
185///     NodeId::named("user:alice"),
186///     Predicate::named("has_name"),
187///     Value::literal("Alice"),
188/// ))?;
189///
190/// let result = db.query()
191///     .subject(NodeId::named("user:alice"))
192///     .execute()?;
193///
194/// assert_eq!(result.len(), 1);
195/// assert!(!result.is_empty());
196/// # Ok(())
197/// # }
198/// ```
199#[derive(Debug, Clone)]
200pub struct QueryResult {
201    /// The list of triples that matched the query, up to the specified limit.
202    pub triples: Vec<Triple>,
203    /// The total number of triples that matched the query, ignoring any limit.
204    pub total_count: usize,
205    /// `true` if there are more results available beyond the returned `triples`.
206    pub has_more: bool,
207}
208
209impl QueryResult {
210    /// Creates a new `QueryResult`.
211    pub fn new(triples: Vec<Triple>) -> Self {
212        let total_count = triples.len();
213        Self {
214            triples,
215            total_count,
216            has_more: false,
217        }
218    }
219
220    /// Returns a reference to the first triple in the result set, if any.
221    pub fn first(&self) -> Option<&Triple> {
222        self.triples.first()
223    }
224
225    /// Returns `true` if the result set is empty.
226    pub fn is_empty(&self) -> bool {
227        self.triples.is_empty()
228    }
229
230    /// Returns the number of triples in the current result set.
231    pub fn len(&self) -> usize {
232        self.triples.len()
233    }
234}
235
236/// A builder for constructing and executing queries against a [`GraphStore`].
237///
238/// Provides a fluent API for building pattern-based queries with optional
239/// pagination through limit and offset.
240///
241/// # Examples
242///
243/// Basic query with subject constraint:
244///
245/// ```
246/// use aingle_graph::{GraphDB, Triple, NodeId, Predicate, Value};
247///
248/// # fn main() -> Result<(), aingle_graph::Error> {
249/// let db = GraphDB::memory()?;
250///
251/// db.insert(Triple::new(
252///     NodeId::named("user:alice"),
253///     Predicate::named("has_age"),
254///     Value::integer(30),
255/// ))?;
256///
257/// let results = db.query()
258///     .subject(NodeId::named("user:alice"))
259///     .execute()?;
260///
261/// assert_eq!(results.len(), 1);
262/// # Ok(())
263/// # }
264/// ```
265///
266/// Query with multiple constraints and pagination:
267///
268/// ```
269/// use aingle_graph::{GraphDB, Triple, NodeId, Predicate, Value};
270///
271/// # fn main() -> Result<(), aingle_graph::Error> {
272/// let db = GraphDB::memory()?;
273///
274/// // Insert multiple triples
275/// for i in 0..20 {
276///     db.insert(Triple::new(
277///         NodeId::named(format!("user:{}", i)),
278///         Predicate::named("has_type"),
279///         Value::literal("user"),
280///     ))?;
281/// }
282///
283/// let results = db.query()
284///     .predicate(Predicate::named("has_type"))
285///     .limit(10)
286///     .offset(5)
287///     .execute()?;
288///
289/// assert_eq!(results.len(), 10);
290/// assert_eq!(results.total_count, 20);
291/// assert!(results.has_more);
292/// # Ok(())
293/// # }
294/// ```
295pub struct QueryBuilder<'a> {
296    store: &'a GraphStore,
297    pattern: TriplePattern,
298    limit: Option<usize>,
299    offset: usize,
300}
301
302impl<'a> QueryBuilder<'a> {
303    /// Creates a new `QueryBuilder` for a given `GraphStore`.
304    pub fn new(store: &'a GraphStore) -> Self {
305        Self {
306            store,
307            pattern: TriplePattern::default(),
308            limit: None,
309            offset: 0,
310        }
311    }
312
313    /// Adds a subject constraint to the query.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use aingle_graph::{GraphDB, NodeId};
319    ///
320    /// # fn main() -> Result<(), aingle_graph::Error> {
321    /// let db = GraphDB::memory()?;
322    ///
323    /// let results = db.query()
324    ///     .subject(NodeId::named("user:alice"))
325    ///     .execute()?;
326    /// # Ok(())
327    /// # }
328    /// ```
329    pub fn subject(mut self, subject: NodeId) -> Self {
330        self.pattern.subject = Some(subject);
331        self
332    }
333
334    /// Adds a predicate constraint to the query.
335    pub fn predicate(mut self, predicate: Predicate) -> Self {
336        self.pattern.predicate = Some(predicate);
337        self
338    }
339
340    /// Adds an object constraint to the query.
341    pub fn object(mut self, object: Value) -> Self {
342        self.pattern.object = Some(object);
343        self
344    }
345
346    /// Sets the maximum number of results to return.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use aingle_graph::{GraphDB, Predicate};
352    ///
353    /// # fn main() -> Result<(), aingle_graph::Error> {
354    /// let db = GraphDB::memory()?;
355    ///
356    /// let results = db.query()
357    ///     .predicate(Predicate::named("has_name"))
358    ///     .limit(10)
359    ///     .execute()?;
360    ///
361    /// assert!(results.len() <= 10);
362    /// # Ok(())
363    /// # }
364    /// ```
365    pub fn limit(mut self, limit: usize) -> Self {
366        self.limit = Some(limit);
367        self
368    }
369
370    /// Sets the number of results to skip from the beginning.
371    ///
372    /// Useful for pagination in combination with [`limit`](Self::limit).
373    ///
374    /// # Examples
375    ///
376    /// ```
377    /// use aingle_graph::{GraphDB, Predicate};
378    ///
379    /// # fn main() -> Result<(), aingle_graph::Error> {
380    /// let db = GraphDB::memory()?;
381    ///
382    /// // Get second page of results
383    /// let results = db.query()
384    ///     .predicate(Predicate::named("has_name"))
385    ///     .limit(10)
386    ///     .offset(10)
387    ///     .execute()?;
388    /// # Ok(())
389    /// # }
390    /// ```
391    pub fn offset(mut self, offset: usize) -> Self {
392        self.offset = offset;
393        self
394    }
395
396    /// Executes the constructed query.
397    pub fn execute(self) -> Result<QueryResult> {
398        let mut triples = self.store.find(self.pattern)?;
399        let total_count = triples.len();
400
401        // Apply offset
402        if self.offset > 0 {
403            if self.offset >= triples.len() {
404                triples.clear();
405            } else {
406                triples = triples.into_iter().skip(self.offset).collect();
407            }
408        }
409
410        // Apply limit
411        let has_more = if let Some(limit) = self.limit {
412            let exceeded = triples.len() > limit;
413            triples.truncate(limit);
414            exceeded
415        } else {
416            false
417        };
418
419        Ok(QueryResult {
420            triples,
421            total_count,
422            has_more,
423        })
424    }
425}
426
427/// A builder for performing graph traversals.
428///
429/// Traversals allow you to explore the graph starting from a node and following
430/// relationships (predicates) to discover connected nodes.
431///
432/// # Examples
433///
434/// ```
435/// use aingle_graph::{GraphDB, Triple, NodeId, Predicate};
436///
437/// # fn main() -> Result<(), aingle_graph::Error> {
438/// let db = GraphDB::memory()?;
439///
440/// // Build a social graph
441/// db.insert(Triple::link(
442///     NodeId::named("alice"),
443///     Predicate::named("knows"),
444///     NodeId::named("bob"),
445/// ))?;
446///
447/// db.insert(Triple::link(
448///     NodeId::named("bob"),
449///     Predicate::named("knows"),
450///     NodeId::named("charlie"),
451/// ))?;
452///
453/// // Traverse from alice following "knows" relationships
454/// let reachable = db.traverse(
455///     &NodeId::named("alice"),
456///     &[Predicate::named("knows")],
457/// )?;
458///
459/// assert!(reachable.contains(&NodeId::named("bob")));
460/// assert!(reachable.contains(&NodeId::named("charlie")));
461/// # Ok(())
462/// # }
463/// ```
464pub struct TraversalBuilder<'a> {
465    store: &'a GraphStore,
466    start: NodeId,
467    predicates: Vec<Predicate>,
468    max_depth: usize,
469    follow_inverse: bool,
470}
471
472impl<'a> TraversalBuilder<'a> {
473    /// Creates a new traversal builder starting from a specific node.
474    pub fn from(store: &'a GraphStore, start: NodeId) -> Self {
475        Self {
476            store,
477            start,
478            predicates: Vec::new(),
479            max_depth: 10,
480            follow_inverse: false,
481        }
482    }
483
484    /// Adds a predicate to follow during the traversal.
485    pub fn follow(mut self, predicate: Predicate) -> Self {
486        self.predicates.push(predicate);
487        self
488    }
489
490    /// Adds multiple predicates to follow during the traversal.
491    pub fn follow_all(mut self, predicates: Vec<Predicate>) -> Self {
492        self.predicates.extend(predicates);
493        self
494    }
495
496    /// Sets the maximum depth for the traversal.
497    pub fn max_depth(mut self, depth: usize) -> Self {
498        self.max_depth = depth;
499        self
500    }
501
502    /// Configures the traversal to also follow inverse relationships (from object to subject).
503    pub fn bidirectional(mut self) -> Self {
504        self.follow_inverse = true;
505        self
506    }
507
508    /// Executes the traversal.
509    pub fn execute(self) -> Result<Vec<NodeId>> {
510        self.store.traverse(&self.start, &self.predicates)
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn test_pattern_matches() {
520        let triple = Triple::new(
521            NodeId::named("user:alice"),
522            Predicate::named("has_name"),
523            Value::literal("Alice"),
524        );
525
526        // Wildcard matches everything
527        assert!(TriplePattern::any().matches(&triple));
528
529        // Subject match
530        assert!(TriplePattern::subject(NodeId::named("user:alice")).matches(&triple));
531        assert!(!TriplePattern::subject(NodeId::named("user:bob")).matches(&triple));
532
533        // Predicate match
534        assert!(TriplePattern::predicate(Predicate::named("has_name")).matches(&triple));
535        assert!(!TriplePattern::predicate(Predicate::named("has_age")).matches(&triple));
536
537        // Object match
538        assert!(TriplePattern::object(Value::literal("Alice")).matches(&triple));
539        assert!(!TriplePattern::object(Value::literal("Bob")).matches(&triple));
540
541        // Combined match
542        let pattern = TriplePattern::subject(NodeId::named("user:alice"))
543            .with_predicate(Predicate::named("has_name"));
544        assert!(pattern.matches(&triple));
545    }
546
547    #[test]
548    fn test_pattern_is_exact() {
549        let partial = TriplePattern::subject(NodeId::named("a"));
550        assert!(!partial.is_exact());
551
552        let exact = TriplePattern::subject(NodeId::named("a"))
553            .with_predicate(Predicate::named("b"))
554            .with_object(Value::literal("c"));
555        assert!(exact.is_exact());
556    }
557
558    #[test]
559    fn test_query_result() {
560        let t1 = Triple::new(
561            NodeId::named("a"),
562            Predicate::named("p"),
563            Value::literal("b"),
564        );
565        let t2 = Triple::new(
566            NodeId::named("c"),
567            Predicate::named("p"),
568            Value::literal("d"),
569        );
570
571        let result = QueryResult::new(vec![t1.clone(), t2]);
572        assert_eq!(result.len(), 2);
573        assert!(!result.is_empty());
574        assert_eq!(result.first().unwrap().subject, t1.subject);
575    }
576}