Skip to main content

rouchdb_query/
mango.rs

1//! Mango query engine — CouchDB-compatible selector-based document queries.
2//!
3//! Supports the standard Mango operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`,
4//! `$lte`, `$in`, `$nin`, `$exists`, `$regex`, `$elemMatch`, `$all`, `$size`,
5//! `$or`, `$and`, `$not`, `$nor`, `$mod`, `$type`.
6
7use std::collections::HashMap;
8
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11
12use rouchdb_core::adapter::Adapter;
13use rouchdb_core::collation::collate;
14use rouchdb_core::document::AllDocsOptions;
15use rouchdb_core::error::Result;
16
17/// Options for a Mango find query.
18#[derive(Debug, Clone, Default, Serialize, Deserialize)]
19pub struct FindOptions {
20    /// The selector (query) to match documents against.
21    pub selector: serde_json::Value,
22    /// Fields to include in the result (projection).
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub fields: Option<Vec<String>>,
25    /// Sort specification.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub sort: Option<Vec<SortField>>,
28    /// Maximum number of results.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub limit: Option<u64>,
31    /// Number of results to skip.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub skip: Option<u64>,
34}
35
36/// A single sort field with direction.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum SortField {
40    /// Simple field name (ascending).
41    Simple(String),
42    /// Field with direction: `{"field": "asc"}` or `{"field": "desc"}`.
43    WithDirection(HashMap<String, String>),
44}
45
46impl SortField {
47    fn field_and_direction(&self) -> (&str, SortDirection) {
48        match self {
49            SortField::Simple(f) => (f.as_str(), SortDirection::Asc),
50            SortField::WithDirection(map) => {
51                let (field, dir) = map.iter().next().unwrap();
52                let direction = if dir == "desc" {
53                    SortDirection::Desc
54                } else {
55                    SortDirection::Asc
56                };
57                (field.as_str(), direction)
58            }
59        }
60    }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
64enum SortDirection {
65    Asc,
66    Desc,
67}
68
69/// Result of a find query.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct FindResponse {
72    pub docs: Vec<serde_json::Value>,
73}
74
75/// Execute a Mango find query against an adapter.
76pub async fn find(adapter: &dyn Adapter, opts: FindOptions) -> Result<FindResponse> {
77    // Fetch all documents
78    let all = adapter
79        .all_docs(AllDocsOptions {
80            include_docs: true,
81            ..AllDocsOptions::new()
82        })
83        .await?;
84
85    let mut matched: Vec<serde_json::Value> = Vec::new();
86
87    for row in &all.rows {
88        if let Some(ref doc_json) = row.doc
89            && matches_selector(doc_json, &opts.selector)
90        {
91            matched.push(doc_json.clone());
92        }
93    }
94
95    // Sort
96    if let Some(ref sort_fields) = opts.sort {
97        matched.sort_by(|a, b| {
98            for sf in sort_fields {
99                let (field, direction) = sf.field_and_direction();
100                let va = get_nested_field(a, field);
101                let vb = get_nested_field(b, field);
102                let va = va.unwrap_or(&serde_json::Value::Null);
103                let vb = vb.unwrap_or(&serde_json::Value::Null);
104                let cmp = collate(va, vb);
105                let cmp = if direction == SortDirection::Desc {
106                    cmp.reverse()
107                } else {
108                    cmp
109                };
110                if cmp != std::cmp::Ordering::Equal {
111                    return cmp;
112                }
113            }
114            std::cmp::Ordering::Equal
115        });
116    }
117
118    // Skip
119    if let Some(skip) = opts.skip {
120        matched = matched.into_iter().skip(skip as usize).collect();
121    }
122
123    // Limit
124    if let Some(limit) = opts.limit {
125        matched.truncate(limit as usize);
126    }
127
128    // Field projection
129    if let Some(ref fields) = opts.fields {
130        matched = matched
131            .into_iter()
132            .map(|doc| project(doc, fields))
133            .collect();
134    }
135
136    Ok(FindResponse { docs: matched })
137}
138
139/// Check if a document matches a Mango selector.
140pub fn matches_selector(doc: &serde_json::Value, selector: &serde_json::Value) -> bool {
141    match selector {
142        serde_json::Value::Object(map) => {
143            for (key, condition) in map {
144                if !match_condition(doc, key, condition) {
145                    return false;
146                }
147            }
148            true
149        }
150        _ => false,
151    }
152}
153
154fn match_condition(doc: &serde_json::Value, key: &str, condition: &serde_json::Value) -> bool {
155    // Check for logical operators
156    match key {
157        "$and" => return match_and(doc, condition),
158        "$or" => return match_or(doc, condition),
159        "$not" => return match_not(doc, condition),
160        "$nor" => return match_nor(doc, condition),
161        _ => {}
162    }
163
164    let field_value = get_nested_field(doc, key);
165
166    match condition {
167        // Shorthand: {"field": value} means {"field": {"$eq": value}}
168        serde_json::Value::Object(ops) => {
169            for (op, operand) in ops {
170                if !match_operator(field_value, op, operand) {
171                    return false;
172                }
173            }
174            true
175        }
176        // Implicit $eq
177        other => match_operator(field_value, "$eq", other),
178    }
179}
180
181fn match_operator(
182    field_value: Option<&serde_json::Value>,
183    op: &str,
184    operand: &serde_json::Value,
185) -> bool {
186    match op {
187        "$eq" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Equal),
188        "$ne" => field_value.is_none_or(|v| collate(v, operand) != std::cmp::Ordering::Equal),
189        "$gt" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Greater),
190        "$gte" => field_value.is_some_and(|v| collate(v, operand) != std::cmp::Ordering::Less),
191        "$lt" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Less),
192        "$lte" => field_value.is_some_and(|v| collate(v, operand) != std::cmp::Ordering::Greater),
193        "$in" => {
194            if let Some(arr) = operand.as_array() {
195                field_value.is_some_and(|v| {
196                    arr.iter()
197                        .any(|item| collate(v, item) == std::cmp::Ordering::Equal)
198                })
199            } else {
200                false
201            }
202        }
203        "$nin" => {
204            if let Some(arr) = operand.as_array() {
205                field_value.is_none_or(|v| {
206                    !arr.iter()
207                        .any(|item| collate(v, item) == std::cmp::Ordering::Equal)
208                })
209            } else {
210                true
211            }
212        }
213        "$exists" => {
214            let should_exist = operand.as_bool().unwrap_or(true);
215            if should_exist {
216                field_value.is_some()
217            } else {
218                field_value.is_none()
219            }
220        }
221        "$type" => {
222            if let Some(type_name) = operand.as_str() {
223                field_value.is_some_and(|v| json_type_name(v) == type_name)
224            } else {
225                false
226            }
227        }
228        "$regex" => {
229            if let Some(pattern) = operand.as_str() {
230                field_value.is_some_and(|v| {
231                    if let Some(s) = v.as_str() {
232                        Regex::new(pattern).is_ok_and(|re| re.is_match(s))
233                    } else {
234                        false
235                    }
236                })
237            } else {
238                false
239            }
240        }
241        "$size" => {
242            if let Some(expected_size) = operand.as_u64() {
243                field_value.is_some_and(|v| {
244                    v.as_array()
245                        .is_some_and(|arr| arr.len() as u64 == expected_size)
246                })
247            } else {
248                false
249            }
250        }
251        "$all" => {
252            if let Some(required) = operand.as_array() {
253                field_value.is_some_and(|v| {
254                    if let Some(arr) = v.as_array() {
255                        required.iter().all(|req| {
256                            arr.iter()
257                                .any(|item| collate(item, req) == std::cmp::Ordering::Equal)
258                        })
259                    } else {
260                        false
261                    }
262                })
263            } else {
264                false
265            }
266        }
267        "$elemMatch" => field_value.is_some_and(|v| {
268            if let Some(arr) = v.as_array() {
269                arr.iter().any(|elem| matches_selector(elem, operand))
270            } else {
271                false
272            }
273        }),
274        "$not" => {
275            // Field-level $not: negate the sub-condition applied to this field's value
276            if let Some(ops) = operand.as_object() {
277                for (sub_op, sub_operand) in ops {
278                    if match_operator(field_value, sub_op, sub_operand) {
279                        return false;
280                    }
281                }
282                true
283            } else {
284                // Implicit $eq negation
285                !match_operator(field_value, "$eq", operand)
286            }
287        }
288        "$mod" => {
289            if let Some(arr) = operand.as_array() {
290                if arr.len() == 2 {
291                    let divisor = arr[0].as_i64();
292                    let remainder = arr[1].as_i64();
293                    if let (Some(d), Some(r)) = (divisor, remainder) {
294                        field_value
295                            .is_some_and(|v| v.as_i64().is_some_and(|n| d != 0 && n % d == r))
296                    } else {
297                        false
298                    }
299                } else {
300                    false
301                }
302            } else {
303                false
304            }
305        }
306        _ => false,
307    }
308}
309
310fn match_and(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
311    if let Some(arr) = condition.as_array() {
312        arr.iter().all(|sub| matches_selector(doc, sub))
313    } else {
314        false
315    }
316}
317
318fn match_or(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
319    if let Some(arr) = condition.as_array() {
320        arr.iter().any(|sub| matches_selector(doc, sub))
321    } else {
322        false
323    }
324}
325
326fn match_not(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
327    !matches_selector(doc, condition)
328}
329
330fn match_nor(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
331    if let Some(arr) = condition.as_array() {
332        !arr.iter().any(|sub| matches_selector(doc, sub))
333    } else {
334        false
335    }
336}
337
338/// Get a nested field from a JSON value using dot notation.
339fn get_nested_field<'a>(doc: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
340    let mut current = doc;
341    for part in path.split('.') {
342        match current.get(part) {
343            Some(v) => current = v,
344            None => return None,
345        }
346    }
347    Some(current)
348}
349
350/// Return the CouchDB type name for a JSON value.
351fn json_type_name(value: &serde_json::Value) -> &'static str {
352    match value {
353        serde_json::Value::Null => "null",
354        serde_json::Value::Bool(_) => "boolean",
355        serde_json::Value::Number(_) => "number",
356        serde_json::Value::String(_) => "string",
357        serde_json::Value::Array(_) => "array",
358        serde_json::Value::Object(_) => "object",
359    }
360}
361
362/// Project a document to only include the specified fields.
363fn project(doc: serde_json::Value, fields: &[String]) -> serde_json::Value {
364    let mut result = serde_json::Map::new();
365
366    if let serde_json::Value::Object(map) = &doc {
367        for field in fields {
368            // Always include _id and _rev
369            if let Some(val) = map.get(field) {
370                result.insert(field.clone(), val.clone());
371            }
372        }
373        // Always include _id
374        if let Some(id) = map.get("_id") {
375            result
376                .entry("_id".to_string())
377                .or_insert_with(|| id.clone());
378        }
379    }
380
381    serde_json::Value::Object(result)
382}
383
384// ---------------------------------------------------------------------------
385// Tests
386// ---------------------------------------------------------------------------
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    fn doc(json: serde_json::Value) -> serde_json::Value {
393        json
394    }
395
396    // --- Basic matching ---
397
398    #[test]
399    fn eq_implicit() {
400        let d = doc(serde_json::json!({"name": "Alice", "age": 30}));
401        assert!(matches_selector(&d, &serde_json::json!({"name": "Alice"})));
402        assert!(!matches_selector(&d, &serde_json::json!({"name": "Bob"})));
403    }
404
405    #[test]
406    fn eq_explicit() {
407        let d = doc(serde_json::json!({"age": 30}));
408        assert!(matches_selector(
409            &d,
410            &serde_json::json!({"age": {"$eq": 30}})
411        ));
412    }
413
414    #[test]
415    fn ne() {
416        let d = doc(serde_json::json!({"age": 30}));
417        assert!(matches_selector(
418            &d,
419            &serde_json::json!({"age": {"$ne": 25}})
420        ));
421        assert!(!matches_selector(
422            &d,
423            &serde_json::json!({"age": {"$ne": 30}})
424        ));
425    }
426
427    #[test]
428    fn gt_gte_lt_lte() {
429        let d = doc(serde_json::json!({"age": 30}));
430
431        assert!(matches_selector(
432            &d,
433            &serde_json::json!({"age": {"$gt": 20}})
434        ));
435        assert!(!matches_selector(
436            &d,
437            &serde_json::json!({"age": {"$gt": 30}})
438        ));
439
440        assert!(matches_selector(
441            &d,
442            &serde_json::json!({"age": {"$gte": 30}})
443        ));
444        assert!(!matches_selector(
445            &d,
446            &serde_json::json!({"age": {"$gte": 31}})
447        ));
448
449        assert!(matches_selector(
450            &d,
451            &serde_json::json!({"age": {"$lt": 40}})
452        ));
453        assert!(!matches_selector(
454            &d,
455            &serde_json::json!({"age": {"$lt": 30}})
456        ));
457
458        assert!(matches_selector(
459            &d,
460            &serde_json::json!({"age": {"$lte": 30}})
461        ));
462        assert!(!matches_selector(
463            &d,
464            &serde_json::json!({"age": {"$lte": 29}})
465        ));
466    }
467
468    #[test]
469    fn in_nin() {
470        let d = doc(serde_json::json!({"color": "red"}));
471
472        assert!(matches_selector(
473            &d,
474            &serde_json::json!({"color": {"$in": ["red", "blue"]}})
475        ));
476        assert!(!matches_selector(
477            &d,
478            &serde_json::json!({"color": {"$in": ["green", "blue"]}})
479        ));
480
481        assert!(matches_selector(
482            &d,
483            &serde_json::json!({"color": {"$nin": ["green", "blue"]}})
484        ));
485        assert!(!matches_selector(
486            &d,
487            &serde_json::json!({"color": {"$nin": ["red", "blue"]}})
488        ));
489    }
490
491    #[test]
492    fn exists() {
493        let d = doc(serde_json::json!({"name": "Alice"}));
494
495        assert!(matches_selector(
496            &d,
497            &serde_json::json!({"name": {"$exists": true}})
498        ));
499        assert!(!matches_selector(
500            &d,
501            &serde_json::json!({"age": {"$exists": true}})
502        ));
503        assert!(matches_selector(
504            &d,
505            &serde_json::json!({"age": {"$exists": false}})
506        ));
507    }
508
509    #[test]
510    fn type_check() {
511        let d = doc(serde_json::json!({"name": "Alice", "age": 30, "active": true}));
512
513        assert!(matches_selector(
514            &d,
515            &serde_json::json!({"name": {"$type": "string"}})
516        ));
517        assert!(matches_selector(
518            &d,
519            &serde_json::json!({"age": {"$type": "number"}})
520        ));
521        assert!(matches_selector(
522            &d,
523            &serde_json::json!({"active": {"$type": "boolean"}})
524        ));
525    }
526
527    #[test]
528    fn regex_match() {
529        let d = doc(serde_json::json!({"name": "Alice"}));
530
531        assert!(matches_selector(
532            &d,
533            &serde_json::json!({"name": {"$regex": "^Ali"}})
534        ));
535        assert!(!matches_selector(
536            &d,
537            &serde_json::json!({"name": {"$regex": "^Bob"}})
538        ));
539    }
540
541    #[test]
542    fn size_operator() {
543        let d = doc(serde_json::json!({"tags": ["a", "b", "c"]}));
544
545        assert!(matches_selector(
546            &d,
547            &serde_json::json!({"tags": {"$size": 3}})
548        ));
549        assert!(!matches_selector(
550            &d,
551            &serde_json::json!({"tags": {"$size": 2}})
552        ));
553    }
554
555    #[test]
556    fn all_operator() {
557        let d = doc(serde_json::json!({"tags": ["a", "b", "c"]}));
558
559        assert!(matches_selector(
560            &d,
561            &serde_json::json!({"tags": {"$all": ["a", "c"]}})
562        ));
563        assert!(!matches_selector(
564            &d,
565            &serde_json::json!({"tags": {"$all": ["a", "d"]}})
566        ));
567    }
568
569    #[test]
570    fn elem_match() {
571        let d = doc(serde_json::json!({
572            "scores": [
573                {"subject": "math", "grade": 90},
574                {"subject": "english", "grade": 75}
575            ]
576        }));
577
578        assert!(matches_selector(
579            &d,
580            &serde_json::json!({"scores": {"$elemMatch": {"subject": "math", "grade": {"$gt": 80}}}})
581        ));
582        assert!(!matches_selector(
583            &d,
584            &serde_json::json!({"scores": {"$elemMatch": {"subject": "math", "grade": {"$gt": 95}}}})
585        ));
586    }
587
588    #[test]
589    fn mod_operator() {
590        let d = doc(serde_json::json!({"n": 10}));
591
592        assert!(matches_selector(
593            &d,
594            &serde_json::json!({"n": {"$mod": [3, 1]}})
595        ));
596        assert!(!matches_selector(
597            &d,
598            &serde_json::json!({"n": {"$mod": [3, 0]}})
599        ));
600    }
601
602    // --- Logical operators ---
603
604    #[test]
605    fn and_operator() {
606        let d = doc(serde_json::json!({"age": 30, "active": true}));
607
608        assert!(matches_selector(
609            &d,
610            &serde_json::json!({"$and": [{"age": {"$gte": 20}}, {"active": true}]})
611        ));
612        assert!(!matches_selector(
613            &d,
614            &serde_json::json!({"$and": [{"age": {"$gte": 20}}, {"active": false}]})
615        ));
616    }
617
618    #[test]
619    fn or_operator() {
620        let d = doc(serde_json::json!({"age": 30}));
621
622        assert!(matches_selector(
623            &d,
624            &serde_json::json!({"$or": [{"age": 30}, {"age": 40}]})
625        ));
626        assert!(!matches_selector(
627            &d,
628            &serde_json::json!({"$or": [{"age": 20}, {"age": 40}]})
629        ));
630    }
631
632    #[test]
633    fn not_operator() {
634        let d = doc(serde_json::json!({"age": 30}));
635
636        assert!(matches_selector(
637            &d,
638            &serde_json::json!({"$not": {"age": 40}})
639        ));
640        assert!(!matches_selector(
641            &d,
642            &serde_json::json!({"$not": {"age": 30}})
643        ));
644    }
645
646    #[test]
647    fn nor_operator() {
648        let d = doc(serde_json::json!({"age": 30}));
649
650        assert!(matches_selector(
651            &d,
652            &serde_json::json!({"$nor": [{"age": 20}, {"age": 40}]})
653        ));
654        assert!(!matches_selector(
655            &d,
656            &serde_json::json!({"$nor": [{"age": 30}, {"age": 40}]})
657        ));
658    }
659
660    // --- Nested fields ---
661
662    #[test]
663    fn nested_field_access() {
664        let d = doc(serde_json::json!({"address": {"city": "NYC", "zip": "10001"}}));
665
666        assert!(matches_selector(
667            &d,
668            &serde_json::json!({"address.city": "NYC"})
669        ));
670        assert!(!matches_selector(
671            &d,
672            &serde_json::json!({"address.city": "LA"})
673        ));
674    }
675
676    // --- Multiple conditions ---
677
678    #[test]
679    fn multiple_field_conditions() {
680        let d = doc(serde_json::json!({"name": "Alice", "age": 30}));
681
682        // Both must match (implicit AND)
683        assert!(matches_selector(
684            &d,
685            &serde_json::json!({"name": "Alice", "age": {"$gte": 25}})
686        ));
687        assert!(!matches_selector(
688            &d,
689            &serde_json::json!({"name": "Alice", "age": {"$gte": 35}})
690        ));
691    }
692
693    #[test]
694    fn combined_operators_on_field() {
695        let d = doc(serde_json::json!({"age": 30}));
696
697        // Range: 20 < age < 40
698        assert!(matches_selector(
699            &d,
700            &serde_json::json!({"age": {"$gt": 20, "$lt": 40}})
701        ));
702        assert!(!matches_selector(
703            &d,
704            &serde_json::json!({"age": {"$gt": 30, "$lt": 40}})
705        ));
706    }
707
708    // --- Projection ---
709
710    #[test]
711    fn project_fields() {
712        let d = serde_json::json!({"_id": "doc1", "_rev": "1-abc", "name": "Alice", "age": 30});
713        let projected = project(d, &["name".to_string()]);
714
715        assert_eq!(projected["_id"], "doc1");
716        assert_eq!(projected["name"], "Alice");
717        assert!(projected.get("age").is_none());
718    }
719
720    // --- Missing fields ---
721
722    #[test]
723    fn missing_field_ne_matches() {
724        // $ne on missing field should match (field != value is true when field doesn't exist)
725        let d = doc(serde_json::json!({"name": "Alice"}));
726        assert!(matches_selector(
727            &d,
728            &serde_json::json!({"age": {"$ne": 30}})
729        ));
730    }
731
732    #[test]
733    fn missing_field_eq_fails() {
734        let d = doc(serde_json::json!({"name": "Alice"}));
735        assert!(!matches_selector(
736            &d,
737            &serde_json::json!({"age": {"$eq": 30}})
738        ));
739    }
740}