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