Skip to main content

sentinel_dbms/
query.rs

1use serde_json::Value;
2use tokio_stream::Stream;
3
4/// Represents a query for filtering documents in a collection.
5///
6/// A query consists of filters, sorting, limits, offsets, and field projections.
7/// Queries are executed in-memory for basic filtering operations.
8#[allow(
9    clippy::missing_docs_in_private_items,
10    reason = "fields are documented with ///"
11)]
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Query {
14    /// List of filters to apply
15    pub filters:    Vec<Filter>,
16    /// Optional sorting (field, order)
17    pub sort:       Option<(String, SortOrder)>,
18    /// Maximum number of results
19    pub limit:      Option<usize>,
20    /// Number of results to skip
21    pub offset:     Option<usize>,
22    /// Fields to include in results (projection)
23    pub projection: Option<Vec<String>>,
24}
25
26/// The result of executing a query.
27pub struct QueryResult {
28    /// The matching documents as a stream
29    pub documents:      std::pin::Pin<Box<dyn Stream<Item = crate::Result<crate::Document>> + Send>>,
30    /// Total number of documents that matched (before limit/offset), None if not known
31    pub total_count:    Option<usize>,
32    /// Time taken to execute the query
33    pub execution_time: std::time::Duration,
34}
35
36/// Sort order for query results.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum SortOrder {
39    /// Ascending order
40    Ascending,
41    /// Descending order
42    Descending,
43}
44
45/// A filter condition for querying documents.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum Filter {
48    /// Equality filter: field == value
49    Equals(String, Value),
50    /// Greater than filter: field > value
51    GreaterThan(String, Value),
52    /// Less than filter: field < value
53    LessThan(String, Value),
54    /// Greater or equal filter: field >= value
55    GreaterOrEqual(String, Value),
56    /// Less or equal filter: field <= value
57    LessOrEqual(String, Value),
58    /// Contains filter: field contains substring (for strings)
59    Contains(String, String),
60    /// Starts with filter: field starts with prefix (for strings)
61    StartsWith(String, String),
62    /// Ends with filter: field ends with suffix (for strings)
63    EndsWith(String, String),
64    /// In filter: field value is in the provided list
65    In(String, Vec<Value>),
66    /// Exists filter: field exists (or doesn't exist if false)
67    Exists(String, bool),
68    /// Logical AND of two filters
69    And(Box<Self>, Box<Self>),
70    /// Logical OR of two filters
71    Or(Box<Self>, Box<Self>),
72}
73
74/// Operator for building filters in the query builder.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum Operator {
77    /// Equality
78    Equals,
79    /// Greater than
80    GreaterThan,
81    /// Less than
82    LessThan,
83    /// Greater or equal
84    GreaterOrEqual,
85    /// Less or equal
86    LessOrEqual,
87    /// Contains substring
88    Contains,
89    /// Starts with prefix
90    StartsWith,
91    /// Ends with suffix
92    EndsWith,
93    /// Value in list
94    In,
95    /// Field exists
96    Exists,
97}
98
99/// Builder pattern for constructing queries.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct QueryBuilder {
102    /// List of filters to apply
103    filters:    Vec<Filter>,
104    /// Optional sorting field and order
105    sort:       Option<(String, SortOrder)>,
106    /// Optional limit on number of results
107    limit:      Option<usize>,
108    /// Optional offset for pagination
109    offset:     Option<usize>,
110    /// Optional projection of fields
111    projection: Option<Vec<String>>,
112}
113
114impl Default for QueryBuilder {
115    fn default() -> Self { Self::new() }
116}
117
118impl QueryBuilder {
119    /// Creates a new empty query builder.
120    pub const fn new() -> Self {
121        Self {
122            filters:    Vec::new(),
123            sort:       None,
124            limit:      None,
125            offset:     None,
126            projection: None,
127        }
128    }
129
130    /// Adds a filter condition to the query.
131    ///
132    /// # Arguments
133    ///
134    /// * `field` - The field name to filter on
135    /// * `op` - The operator to use
136    /// * `value` - The value to compare against
137    ///
138    /// # Returns
139    ///
140    /// Returns the query builder for chaining.
141    ///
142    /// # Example
143    ///
144    /// ```rust
145    /// use sentinel_dbms::{Operator, QueryBuilder};
146    /// use serde_json::json;
147    ///
148    /// let query = QueryBuilder::new()
149    ///     .filter("age", Operator::GreaterThan, json!(18))
150    ///     .filter("status", Operator::Equals, json!("active"));
151    /// ```
152    pub fn filter(mut self, field: &str, op: Operator, value: Value) -> Self {
153        let filter = match op {
154            Operator::Equals => Filter::Equals(field.to_owned(), value),
155            Operator::GreaterThan => Filter::GreaterThan(field.to_owned(), value),
156            Operator::LessThan => Filter::LessThan(field.to_owned(), value),
157            Operator::GreaterOrEqual => Filter::GreaterOrEqual(field.to_owned(), value),
158            Operator::LessOrEqual => Filter::LessOrEqual(field.to_owned(), value),
159            Operator::Contains => {
160                if let Value::String(s) = value {
161                    Filter::Contains(field.to_owned(), s)
162                }
163                else {
164                    // Invalid type for contains, ignore or handle error
165                    return self;
166                }
167            },
168            Operator::StartsWith => {
169                if let Value::String(s) = value {
170                    Filter::StartsWith(field.to_owned(), s)
171                }
172                else {
173                    return self;
174                }
175            },
176            Operator::EndsWith => {
177                if let Value::String(s) = value {
178                    Filter::EndsWith(field.to_owned(), s)
179                }
180                else {
181                    return self;
182                }
183            },
184            Operator::In => {
185                if let Value::Array(arr) = value {
186                    Filter::In(field.to_owned(), arr)
187                }
188                else {
189                    return self;
190                }
191            },
192            Operator::Exists => {
193                let exists = match value {
194                    Value::Bool(b) => b,
195                    Value::Number(n) if n.as_i64() == Some(1) => true,
196                    Value::Number(n) if n.as_i64() == Some(0) => false,
197                    Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_) | Value::Object(_) => true, /* Default to exists */
198                };
199                Filter::Exists(field.to_owned(), exists)
200            },
201        };
202        self.filters.push(filter);
203        self
204    }
205
206    /// Adds a logical AND filter combining the current filters.
207    ///
208    /// # Arguments
209    ///
210    /// * `other` - Another filter to AND with the current query
211    ///
212    /// # Returns
213    ///
214    /// Returns the query builder for chaining.
215    pub fn and(mut self, other: Filter) -> Self {
216        if let Some(last) = self.filters.pop() {
217            let combined = Filter::And(Box::new(last), Box::new(other));
218            self.filters.push(combined);
219        }
220        else {
221            self.filters.push(other);
222        }
223        self
224    }
225
226    /// Adds a logical OR filter combining the current filters.
227    ///
228    /// # Arguments
229    ///
230    /// * `other` - Another filter to OR with the current query
231    ///
232    /// # Returns
233    ///
234    /// Returns the query builder for chaining.
235    pub fn or(mut self, other: Filter) -> Self {
236        if let Some(last) = self.filters.pop() {
237            let combined = Filter::Or(Box::new(last), Box::new(other));
238            self.filters.push(combined);
239        }
240        else {
241            self.filters.push(other);
242        }
243        self
244    }
245
246    /// Sets the sort order for the query results.
247    ///
248    /// # Arguments
249    ///
250    /// * `field` - The field to sort by
251    /// * `order` - The sort order (ascending or descending)
252    ///
253    /// # Returns
254    ///
255    /// Returns the query builder for chaining.
256    ///
257    /// # Example
258    ///
259    /// ```rust
260    /// use sentinel_dbms::{QueryBuilder, SortOrder};
261    ///
262    /// let query = QueryBuilder::new().sort("age", SortOrder::Descending);
263    /// ```
264    pub fn sort(mut self, field: &str, order: SortOrder) -> Self {
265        self.sort = Some((field.to_owned(), order));
266        self
267    }
268
269    /// Sets the maximum number of results to return.
270    ///
271    /// # Arguments
272    ///
273    /// * `limit` - Maximum number of documents to return
274    ///
275    /// # Returns
276    ///
277    /// Returns the query builder for chaining.
278    pub const fn limit(mut self, limit: usize) -> Self {
279        self.limit = Some(limit);
280        self
281    }
282
283    /// Sets the number of results to skip.
284    ///
285    /// # Arguments
286    ///
287    /// * `offset` - Number of documents to skip
288    ///
289    /// # Returns
290    ///
291    /// Returns the query builder for chaining.
292    pub const fn offset(mut self, offset: usize) -> Self {
293        self.offset = Some(offset);
294        self
295    }
296
297    /// Sets the fields to include in the results (projection).
298    ///
299    /// If projection is set, only the specified fields will be included
300    /// in the returned documents. If not set, all fields are included.
301    ///
302    /// # Arguments
303    ///
304    /// * `fields` - List of field names to include
305    ///
306    /// # Returns
307    ///
308    /// Returns the query builder for chaining.
309    ///
310    /// # Example
311    ///
312    /// ```rust
313    /// use sentinel_dbms::QueryBuilder;
314    ///
315    /// let query = QueryBuilder::new().projection(vec!["name", "email"]);
316    /// ```
317    pub fn projection(mut self, fields: Vec<&str>) -> Self {
318        self.projection = Some(fields.into_iter().map(|s| s.to_owned()).collect());
319        self
320    }
321
322    /// Builds the query from the current builder state.
323    ///
324    /// # Returns
325    ///
326    /// Returns a `Query` that can be executed against a collection.
327    pub fn build(self) -> Query {
328        Query {
329            filters:    self.filters,
330            sort:       self.sort,
331            limit:      self.limit,
332            offset:     self.offset,
333            projection: self.projection,
334        }
335    }
336}
337
338/// Aggregation operations for queries.
339#[derive(Debug, Clone, PartialEq, Eq)]
340pub enum Aggregation {
341    /// Count of matching documents
342    Count,
343    /// Sum of numeric values in the specified field
344    Sum(String),
345    /// Average of numeric values in the specified field
346    Avg(String),
347    /// Minimum value in the specified field
348    Min(String),
349    /// Maximum value in the specified field
350    Max(String),
351}
352
353#[cfg(test)]
354mod tests {
355    use serde_json::json;
356
357    use super::*;
358
359    #[test]
360    fn test_query_builder_new() {
361        let qb = QueryBuilder::new();
362        assert!(qb.filters.is_empty());
363        assert!(qb.sort.is_none());
364        assert!(qb.limit.is_none());
365        assert!(qb.offset.is_none());
366        assert!(qb.projection.is_none());
367    }
368
369    #[test]
370    fn test_query_builder_default() {
371        let qb = QueryBuilder::default();
372        assert!(qb.filters.is_empty());
373        assert!(qb.sort.is_none());
374        assert!(qb.limit.is_none());
375        assert!(qb.offset.is_none());
376        assert!(qb.projection.is_none());
377    }
378
379    #[test]
380    fn test_query_builder_filter_equals() {
381        let qb = QueryBuilder::new().filter("name", Operator::Equals, json!("Alice"));
382        assert_eq!(qb.filters.len(), 1);
383        match &qb.filters[0] {
384            Filter::Equals(field, value) => {
385                assert_eq!(field, "name");
386                assert_eq!(value, &json!("Alice"));
387            },
388            _ => panic!("Expected Equals filter"),
389        }
390    }
391
392    #[test]
393    fn test_query_builder_filter_greater_than() {
394        let qb = QueryBuilder::new().filter("age", Operator::GreaterThan, json!(18));
395        assert_eq!(qb.filters.len(), 1);
396        match &qb.filters[0] {
397            Filter::GreaterThan(field, value) => {
398                assert_eq!(field, "age");
399                assert_eq!(value, &json!(18));
400            },
401            _ => panic!("Expected GreaterThan filter"),
402        }
403    }
404
405    #[test]
406    fn test_query_builder_filter_less_than() {
407        let qb = QueryBuilder::new().filter("age", Operator::LessThan, json!(65));
408        assert_eq!(qb.filters.len(), 1);
409        match &qb.filters[0] {
410            Filter::LessThan(field, value) => {
411                assert_eq!(field, "age");
412                assert_eq!(value, &json!(65));
413            },
414            _ => panic!("Expected LessThan filter"),
415        }
416    }
417
418    #[test]
419    fn test_query_builder_filter_greater_or_equal() {
420        let qb = QueryBuilder::new().filter("age", Operator::GreaterOrEqual, json!(18));
421        assert_eq!(qb.filters.len(), 1);
422        match &qb.filters[0] {
423            Filter::GreaterOrEqual(field, value) => {
424                assert_eq!(field, "age");
425                assert_eq!(value, &json!(18));
426            },
427            _ => panic!("Expected GreaterOrEqual filter"),
428        }
429    }
430
431    #[test]
432    fn test_query_builder_filter_less_or_equal() {
433        let qb = QueryBuilder::new().filter("age", Operator::LessOrEqual, json!(65));
434        assert_eq!(qb.filters.len(), 1);
435        match &qb.filters[0] {
436            Filter::LessOrEqual(field, value) => {
437                assert_eq!(field, "age");
438                assert_eq!(value, &json!(65));
439            },
440            _ => panic!("Expected LessOrEqual filter"),
441        }
442    }
443
444    #[test]
445    fn test_query_builder_filter_contains_valid() {
446        let qb = QueryBuilder::new().filter("name", Operator::Contains, json!("Ali"));
447        assert_eq!(qb.filters.len(), 1);
448        match &qb.filters[0] {
449            Filter::Contains(field, value) => {
450                assert_eq!(field, "name");
451                assert_eq!(value, "Ali");
452            },
453            _ => panic!("Expected Contains filter"),
454        }
455    }
456
457    #[test]
458    fn test_query_builder_filter_contains_invalid() {
459        let qb = QueryBuilder::new().filter("name", Operator::Contains, json!(123));
460        assert!(qb.filters.is_empty());
461    }
462
463    #[test]
464    fn test_query_builder_filter_starts_with_valid() {
465        let qb = QueryBuilder::new().filter("name", Operator::StartsWith, json!("Ali"));
466        assert_eq!(qb.filters.len(), 1);
467        match &qb.filters[0] {
468            Filter::StartsWith(field, value) => {
469                assert_eq!(field, "name");
470                assert_eq!(value, "Ali");
471            },
472            _ => panic!("Expected StartsWith filter"),
473        }
474    }
475
476    #[test]
477    fn test_query_builder_filter_starts_with_invalid() {
478        let qb = QueryBuilder::new().filter("name", Operator::StartsWith, json!(123));
479        assert!(qb.filters.is_empty());
480    }
481
482    #[test]
483    fn test_query_builder_filter_ends_with_valid() {
484        let qb = QueryBuilder::new().filter("name", Operator::EndsWith, json!("ice"));
485        assert_eq!(qb.filters.len(), 1);
486        match &qb.filters[0] {
487            Filter::EndsWith(field, value) => {
488                assert_eq!(field, "name");
489                assert_eq!(value, "ice");
490            },
491            _ => panic!("Expected EndsWith filter"),
492        }
493    }
494
495    #[test]
496    fn test_query_builder_filter_ends_with_invalid() {
497        let qb = QueryBuilder::new().filter("name", Operator::EndsWith, json!(123));
498        assert!(qb.filters.is_empty());
499    }
500
501    #[test]
502    fn test_query_builder_filter_in_valid() {
503        let qb = QueryBuilder::new().filter("status", Operator::In, json!(["active", "inactive"]));
504        assert_eq!(qb.filters.len(), 1);
505        match &qb.filters[0] {
506            Filter::In(field, values) => {
507                assert_eq!(field, "status");
508                assert_eq!(values, &vec![json!("active"), json!("inactive")]);
509            },
510            _ => panic!("Expected In filter"),
511        }
512    }
513
514    #[test]
515    fn test_query_builder_filter_in_invalid() {
516        let qb = QueryBuilder::new().filter("status", Operator::In, json!("active"));
517        assert!(qb.filters.is_empty());
518    }
519
520    #[test]
521    fn test_query_builder_filter_exists_bool() {
522        let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(true));
523        assert_eq!(qb.filters.len(), 1);
524        match &qb.filters[0] {
525            Filter::Exists(field, exists) => {
526                assert_eq!(field, "name");
527                assert!(*exists);
528            },
529            _ => panic!("Expected Exists filter"),
530        }
531    }
532
533    #[test]
534    fn test_query_builder_filter_exists_number() {
535        let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(1));
536        assert_eq!(qb.filters.len(), 1);
537        match &qb.filters[0] {
538            Filter::Exists(field, exists) => {
539                assert_eq!(field, "name");
540                assert!(*exists);
541            },
542            _ => panic!("Expected Exists filter"),
543        }
544    }
545
546    #[test]
547    fn test_query_builder_filter_exists_false() {
548        let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(false));
549        assert_eq!(qb.filters.len(), 1);
550        match &qb.filters[0] {
551            Filter::Exists(field, exists) => {
552                assert_eq!(field, "name");
553                assert!(!*exists);
554            },
555            _ => panic!("Expected Exists filter"),
556        }
557    }
558
559    #[test]
560    fn test_query_builder_filter_exists_string() {
561        let qb = QueryBuilder::new().filter("name", Operator::Exists, json!("yes"));
562        assert_eq!(qb.filters.len(), 1);
563        match &qb.filters[0] {
564            Filter::Exists(field, exists) => {
565                assert_eq!(field, "name");
566                assert!(*exists); // Default to exists for non-bool/number values
567            },
568            _ => panic!("Expected Exists filter"),
569        }
570    }
571
572    #[test]
573    fn test_query_builder_sort() {
574        let qb = QueryBuilder::new().sort("age", SortOrder::Descending);
575        assert_eq!(qb.sort, Some(("age".to_string(), SortOrder::Descending)));
576    }
577
578    #[test]
579    fn test_query_builder_limit() {
580        let qb = QueryBuilder::new().limit(10);
581        assert_eq!(qb.limit, Some(10));
582    }
583
584    #[test]
585    fn test_query_builder_offset() {
586        let qb = QueryBuilder::new().offset(5);
587        assert_eq!(qb.offset, Some(5));
588    }
589
590    #[test]
591    fn test_query_builder_projection() {
592        let qb = QueryBuilder::new().projection(vec!["name", "age"]);
593        assert_eq!(
594            qb.projection,
595            Some(vec!["name".to_string(), "age".to_string()])
596        );
597    }
598
599    #[test]
600    fn test_query_builder_and() {
601        let qb = QueryBuilder::new()
602            .filter("age", Operator::GreaterThan, json!(18))
603            .and(Filter::Equals("status".to_string(), json!("active")));
604        assert_eq!(qb.filters.len(), 1);
605        match &qb.filters[0] {
606            Filter::And(left, right) => {
607                match **left {
608                    Filter::GreaterThan(ref field, _) => assert_eq!(field, "age"),
609                    _ => panic!("Expected GreaterThan in left"),
610                }
611                match **right {
612                    Filter::Equals(ref field, _) => assert_eq!(field, "status"),
613                    _ => panic!("Expected Equals in right"),
614                }
615            },
616            _ => panic!("Expected And filter"),
617        }
618    }
619
620    #[test]
621    fn test_query_builder_or() {
622        let qb = QueryBuilder::new()
623            .filter("age", Operator::GreaterThan, json!(18))
624            .or(Filter::Equals("status".to_string(), json!("active")));
625        assert_eq!(qb.filters.len(), 1);
626        match &qb.filters[0] {
627            Filter::Or(left, right) => {
628                match **left {
629                    Filter::GreaterThan(ref field, _) => assert_eq!(field, "age"),
630                    _ => panic!("Expected GreaterThan in left"),
631                }
632                match **right {
633                    Filter::Equals(ref field, _) => assert_eq!(field, "status"),
634                    _ => panic!("Expected Equals in right"),
635                }
636            },
637            _ => panic!("Expected Or filter"),
638        }
639    }
640
641    #[test]
642    fn test_query_builder_build() {
643        let query = QueryBuilder::new()
644            .filter("age", Operator::GreaterThan, json!(18))
645            .sort("name", SortOrder::Ascending)
646            .limit(10)
647            .offset(5)
648            .projection(vec!["name", "age"])
649            .build();
650
651        assert_eq!(query.filters.len(), 1);
652        assert_eq!(query.sort, Some(("name".to_string(), SortOrder::Ascending)));
653        assert_eq!(query.limit, Some(10));
654        assert_eq!(query.offset, Some(5));
655        assert_eq!(
656            query.projection,
657            Some(vec!["name".to_string(), "age".to_string()])
658        );
659    }
660
661    #[test]
662    fn test_query_builder_filter_exists_number_zero() {
663        let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(0));
664        assert_eq!(qb.filters.len(), 1);
665        match &qb.filters[0] {
666            Filter::Exists(field, exists) => {
667                assert_eq!(field, "name");
668                assert!(!*exists);
669            },
670            _ => panic!("Expected Exists filter"),
671        }
672    }
673
674    #[test]
675    fn test_query_builder_and_empty() {
676        let qb = QueryBuilder::new().and(Filter::Equals("status".to_string(), json!("active")));
677        assert_eq!(qb.filters.len(), 1);
678        match &qb.filters[0] {
679            Filter::Equals(field, _) => assert_eq!(field, "status"),
680            _ => panic!("Expected Equals filter"),
681        }
682    }
683
684    #[test]
685    fn test_query_builder_or_empty() {
686        let qb = QueryBuilder::new().or(Filter::Equals("status".to_string(), json!("active")));
687        assert_eq!(qb.filters.len(), 1);
688        match &qb.filters[0] {
689            Filter::Equals(field, _) => assert_eq!(field, "status"),
690            _ => panic!("Expected Equals filter"),
691        }
692    }
693}