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}