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/// Definition of a Mango index.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct IndexDefinition {
20    /// Index name (auto-generated if not provided).
21    pub name: String,
22    /// Fields to index, in order.
23    pub fields: Vec<SortField>,
24    /// Optional design document name.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub ddoc: Option<String>,
27}
28
29/// Information about an existing index.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct IndexInfo {
32    /// Index name.
33    pub name: String,
34    /// Design document ID (if any).
35    pub ddoc: Option<String>,
36    /// Indexed fields.
37    pub def: IndexFields,
38}
39
40/// The fields portion of an index definition.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct IndexFields {
43    pub fields: Vec<SortField>,
44}
45
46/// Result of creating an index.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CreateIndexResponse {
49    /// `"created"` or `"exists"`.
50    pub result: String,
51    /// Index name.
52    pub name: String,
53}
54
55/// Response from `explain()` describing how a query would be executed.
56#[derive(Debug, Clone, Serialize)]
57pub struct ExplainResponse {
58    pub dbname: String,
59    pub index: ExplainIndex,
60    pub selector: serde_json::Value,
61    pub fields: Option<Vec<String>>,
62}
63
64/// Description of the index used by a query.
65#[derive(Debug, Clone, Serialize)]
66pub struct ExplainIndex {
67    pub ddoc: Option<String>,
68    pub name: String,
69    #[serde(rename = "type")]
70    pub index_type: String,
71    pub def: IndexFields,
72}
73
74/// A built in-memory index: sorted entries of (composite_key, doc_id).
75#[derive(Debug, Clone)]
76pub struct BuiltIndex {
77    pub def: IndexDefinition,
78    pub entries: Vec<(Vec<serde_json::Value>, String)>,
79}
80
81impl BuiltIndex {
82    /// Find doc IDs matching a simple equality/range selector on the indexed fields.
83    pub fn find_matching(&self, selector: &serde_json::Value) -> Vec<String> {
84        if self.def.fields.is_empty() {
85            return Vec::new();
86        }
87
88        // Extract the first indexed field and its conditions
89        let (first_field, _) = self.def.fields[0].field_and_direction();
90
91        if let Some(conditions) = selector.get(first_field) {
92            match conditions {
93                serde_json::Value::Object(ops) => {
94                    // Range query: use binary search
95                    self.entries
96                        .iter()
97                        .filter(|(key, _)| {
98                            if key.is_empty() {
99                                return false;
100                            }
101                            let val = &key[0];
102                            for (op, operand) in ops {
103                                let matches = match op.as_str() {
104                                    "$eq" => collate(val, operand) == std::cmp::Ordering::Equal,
105                                    "$gt" => collate(val, operand) == std::cmp::Ordering::Greater,
106                                    "$gte" => collate(val, operand) != std::cmp::Ordering::Less,
107                                    "$lt" => collate(val, operand) == std::cmp::Ordering::Less,
108                                    "$lte" => collate(val, operand) != std::cmp::Ordering::Greater,
109                                    _ => true, // Unknown op, don't filter
110                                };
111                                if !matches {
112                                    return false;
113                                }
114                            }
115                            true
116                        })
117                        .map(|(_, id)| id.clone())
118                        .collect()
119                }
120                // Implicit $eq
121                other => self
122                    .entries
123                    .iter()
124                    .filter(|(key, _)| {
125                        !key.is_empty() && collate(&key[0], other) == std::cmp::Ordering::Equal
126                    })
127                    .map(|(_, id)| id.clone())
128                    .collect(),
129            }
130        } else {
131            // Selector doesn't use the indexed field, can't use index
132            self.entries.iter().map(|(_, id)| id.clone()).collect()
133        }
134    }
135}
136
137/// Build an index from all documents in an adapter.
138pub async fn build_index(adapter: &dyn Adapter, def: &IndexDefinition) -> Result<BuiltIndex> {
139    let all = adapter
140        .all_docs(AllDocsOptions {
141            include_docs: true,
142            ..AllDocsOptions::new()
143        })
144        .await?;
145
146    let mut entries: Vec<(Vec<serde_json::Value>, String)> = Vec::new();
147
148    for row in &all.rows {
149        if let Some(ref doc_json) = row.doc {
150            let key: Vec<serde_json::Value> = def
151                .fields
152                .iter()
153                .map(|sf| {
154                    let (field, _) = sf.field_and_direction();
155                    get_nested_field(doc_json, field)
156                        .cloned()
157                        .unwrap_or(serde_json::Value::Null)
158                })
159                .collect();
160            entries.push((key, row.id.clone()));
161        }
162    }
163
164    // Sort by composite key
165    entries.sort_by(|(a, _), (b, _)| {
166        for (va, vb) in a.iter().zip(b.iter()) {
167            let cmp = collate(va, vb);
168            if cmp != std::cmp::Ordering::Equal {
169                return cmp;
170            }
171        }
172        std::cmp::Ordering::Equal
173    });
174
175    Ok(BuiltIndex {
176        def: def.clone(),
177        entries,
178    })
179}
180
181/// Options for a Mango find query.
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct FindOptions {
184    /// The selector (query) to match documents against.
185    pub selector: serde_json::Value,
186    /// Fields to include in the result (projection).
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub fields: Option<Vec<String>>,
189    /// Sort specification.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub sort: Option<Vec<SortField>>,
192    /// Maximum number of results.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub limit: Option<u64>,
195    /// Number of results to skip.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub skip: Option<u64>,
198}
199
200/// A single sort field with direction.
201#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(untagged)]
203pub enum SortField {
204    /// Simple field name (ascending).
205    Simple(String),
206    /// Field with direction: `{"field": "asc"}` or `{"field": "desc"}`.
207    WithDirection(HashMap<String, String>),
208}
209
210impl SortField {
211    pub fn field_and_direction(&self) -> (&str, SortDirection) {
212        match self {
213            SortField::Simple(f) => (f.as_str(), SortDirection::Asc),
214            SortField::WithDirection(map) => {
215                let (field, dir) = map.iter().next().unwrap();
216                let direction = if dir == "desc" {
217                    SortDirection::Desc
218                } else {
219                    SortDirection::Asc
220                };
221                (field.as_str(), direction)
222            }
223        }
224    }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq)]
228pub enum SortDirection {
229    Asc,
230    Desc,
231}
232
233/// Result of a find query.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct FindResponse {
236    pub docs: Vec<serde_json::Value>,
237}
238
239/// Execute a Mango find query against an adapter.
240pub async fn find(adapter: &dyn Adapter, opts: FindOptions) -> Result<FindResponse> {
241    // Fetch all documents
242    let all = adapter
243        .all_docs(AllDocsOptions {
244            include_docs: true,
245            ..AllDocsOptions::new()
246        })
247        .await?;
248
249    let mut matched: Vec<serde_json::Value> = Vec::new();
250
251    for row in &all.rows {
252        if let Some(ref doc_json) = row.doc
253            && matches_selector(doc_json, &opts.selector)
254        {
255            matched.push(doc_json.clone());
256        }
257    }
258
259    // Sort
260    if let Some(ref sort_fields) = opts.sort {
261        matched.sort_by(|a, b| {
262            for sf in sort_fields {
263                let (field, direction) = sf.field_and_direction();
264                let va = get_nested_field(a, field);
265                let vb = get_nested_field(b, field);
266                let va = va.unwrap_or(&serde_json::Value::Null);
267                let vb = vb.unwrap_or(&serde_json::Value::Null);
268                let cmp = collate(va, vb);
269                let cmp = if direction == SortDirection::Desc {
270                    cmp.reverse()
271                } else {
272                    cmp
273                };
274                if cmp != std::cmp::Ordering::Equal {
275                    return cmp;
276                }
277            }
278            std::cmp::Ordering::Equal
279        });
280    }
281
282    // Skip
283    if let Some(skip) = opts.skip {
284        matched = matched.into_iter().skip(skip as usize).collect();
285    }
286
287    // Limit
288    if let Some(limit) = opts.limit {
289        matched.truncate(limit as usize);
290    }
291
292    // Field projection
293    if let Some(ref fields) = opts.fields {
294        matched = matched
295            .into_iter()
296            .map(|doc| project(doc, fields))
297            .collect();
298    }
299
300    Ok(FindResponse { docs: matched })
301}
302
303/// Check if a document matches a Mango selector.
304pub fn matches_selector(doc: &serde_json::Value, selector: &serde_json::Value) -> bool {
305    match selector {
306        serde_json::Value::Object(map) => {
307            for (key, condition) in map {
308                if !match_condition(doc, key, condition) {
309                    return false;
310                }
311            }
312            true
313        }
314        _ => false,
315    }
316}
317
318fn match_condition(doc: &serde_json::Value, key: &str, condition: &serde_json::Value) -> bool {
319    // Check for logical operators
320    match key {
321        "$and" => return match_and(doc, condition),
322        "$or" => return match_or(doc, condition),
323        "$not" => return match_not(doc, condition),
324        "$nor" => return match_nor(doc, condition),
325        _ => {}
326    }
327
328    let field_value = get_nested_field(doc, key);
329
330    match condition {
331        // Shorthand: {"field": value} means {"field": {"$eq": value}}
332        serde_json::Value::Object(ops) => {
333            for (op, operand) in ops {
334                if !match_operator(field_value, op, operand) {
335                    return false;
336                }
337            }
338            true
339        }
340        // Implicit $eq
341        other => match_operator(field_value, "$eq", other),
342    }
343}
344
345/// Match a single `$elemMatch` array element against the operand.
346///
347/// CouchDB allows the operand to be either an operator expression applied
348/// directly to the element (e.g. `{"$gt": 80}` over `[50, 85, 60]`), a
349/// sub-document selector (e.g. `{"subject": "math"}` over array-of-objects),
350/// or a bare scalar (implicit `$eq`).
351fn elem_matches(elem: &serde_json::Value, operand: &serde_json::Value) -> bool {
352    if let Some(map) = operand.as_object() {
353        if !map.is_empty() && map.keys().all(|k| k.starts_with('$')) {
354            return map
355                .iter()
356                .all(|(op, sub)| match_operator(Some(elem), op, sub));
357        }
358        return matches_selector(elem, operand);
359    }
360    match_operator(Some(elem), "$eq", operand)
361}
362
363fn match_operator(
364    field_value: Option<&serde_json::Value>,
365    op: &str,
366    operand: &serde_json::Value,
367) -> bool {
368    match op {
369        "$eq" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Equal),
370        "$ne" => field_value.is_none_or(|v| collate(v, operand) != std::cmp::Ordering::Equal),
371        "$gt" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Greater),
372        "$gte" => field_value.is_some_and(|v| collate(v, operand) != std::cmp::Ordering::Less),
373        "$lt" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Less),
374        "$lte" => field_value.is_some_and(|v| collate(v, operand) != std::cmp::Ordering::Greater),
375        "$in" => {
376            if let Some(arr) = operand.as_array() {
377                field_value.is_some_and(|v| {
378                    arr.iter()
379                        .any(|item| collate(v, item) == std::cmp::Ordering::Equal)
380                })
381            } else {
382                false
383            }
384        }
385        "$nin" => {
386            if let Some(arr) = operand.as_array() {
387                field_value.is_none_or(|v| {
388                    !arr.iter()
389                        .any(|item| collate(v, item) == std::cmp::Ordering::Equal)
390                })
391            } else {
392                true
393            }
394        }
395        "$exists" => {
396            let should_exist = operand.as_bool().unwrap_or(true);
397            if should_exist {
398                field_value.is_some()
399            } else {
400                field_value.is_none()
401            }
402        }
403        "$type" => {
404            if let Some(type_name) = operand.as_str() {
405                field_value.is_some_and(|v| json_type_name(v) == type_name)
406            } else {
407                false
408            }
409        }
410        "$regex" => {
411            if let Some(pattern) = operand.as_str() {
412                field_value.is_some_and(|v| {
413                    if let Some(s) = v.as_str() {
414                        Regex::new(pattern).is_ok_and(|re| re.is_match(s))
415                    } else {
416                        false
417                    }
418                })
419            } else {
420                false
421            }
422        }
423        "$size" => {
424            if let Some(expected_size) = operand.as_u64() {
425                field_value.is_some_and(|v| {
426                    v.as_array()
427                        .is_some_and(|arr| arr.len() as u64 == expected_size)
428                })
429            } else {
430                false
431            }
432        }
433        "$all" => {
434            if let Some(required) = operand.as_array() {
435                field_value.is_some_and(|v| {
436                    if let Some(arr) = v.as_array() {
437                        required.iter().all(|req| {
438                            arr.iter()
439                                .any(|item| collate(item, req) == std::cmp::Ordering::Equal)
440                        })
441                    } else {
442                        false
443                    }
444                })
445            } else {
446                false
447            }
448        }
449        "$elemMatch" => field_value.is_some_and(|v| {
450            if let Some(arr) = v.as_array() {
451                arr.iter().any(|elem| elem_matches(elem, operand))
452            } else {
453                false
454            }
455        }),
456        "$not" => {
457            // Field-level $not: negate the sub-condition applied to this field's value
458            if let Some(ops) = operand.as_object() {
459                for (sub_op, sub_operand) in ops {
460                    if match_operator(field_value, sub_op, sub_operand) {
461                        return false;
462                    }
463                }
464                true
465            } else {
466                // Implicit $eq negation
467                !match_operator(field_value, "$eq", operand)
468            }
469        }
470        "$mod" => {
471            if let Some(arr) = operand.as_array() {
472                if arr.len() == 2 {
473                    let divisor = arr[0].as_i64();
474                    let remainder = arr[1].as_i64();
475                    if let (Some(d), Some(r)) = (divisor, remainder) {
476                        field_value
477                            .is_some_and(|v| v.as_i64().is_some_and(|n| d != 0 && n % d == r))
478                    } else {
479                        false
480                    }
481                } else {
482                    false
483                }
484            } else {
485                false
486            }
487        }
488        _ => false,
489    }
490}
491
492fn match_and(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
493    if let Some(arr) = condition.as_array() {
494        arr.iter().all(|sub| matches_selector(doc, sub))
495    } else {
496        false
497    }
498}
499
500fn match_or(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
501    if let Some(arr) = condition.as_array() {
502        arr.iter().any(|sub| matches_selector(doc, sub))
503    } else {
504        false
505    }
506}
507
508fn match_not(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
509    !matches_selector(doc, condition)
510}
511
512fn match_nor(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
513    if let Some(arr) = condition.as_array() {
514        !arr.iter().any(|sub| matches_selector(doc, sub))
515    } else {
516        false
517    }
518}
519
520/// Get a nested field from a JSON value using dot notation.
521pub fn get_nested_field<'a>(
522    doc: &'a serde_json::Value,
523    path: &str,
524) -> Option<&'a serde_json::Value> {
525    let mut current = doc;
526    for part in path.split('.') {
527        match current.get(part) {
528            Some(v) => current = v,
529            None => return None,
530        }
531    }
532    Some(current)
533}
534
535/// Return the CouchDB type name for a JSON value.
536fn json_type_name(value: &serde_json::Value) -> &'static str {
537    match value {
538        serde_json::Value::Null => "null",
539        serde_json::Value::Bool(_) => "boolean",
540        serde_json::Value::Number(_) => "number",
541        serde_json::Value::String(_) => "string",
542        serde_json::Value::Array(_) => "array",
543        serde_json::Value::Object(_) => "object",
544    }
545}
546
547/// Project a document to only include the specified fields.
548fn project(doc: serde_json::Value, fields: &[String]) -> serde_json::Value {
549    let mut result = serde_json::Map::new();
550
551    if let serde_json::Value::Object(map) = &doc {
552        for field in fields {
553            // Always include _id and _rev
554            if let Some(val) = map.get(field) {
555                result.insert(field.clone(), val.clone());
556            }
557        }
558        // Always include _id
559        if let Some(id) = map.get("_id") {
560            result
561                .entry("_id".to_string())
562                .or_insert_with(|| id.clone());
563        }
564    }
565
566    serde_json::Value::Object(result)
567}
568
569// ---------------------------------------------------------------------------
570// Tests
571// ---------------------------------------------------------------------------
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576
577    fn doc(json: serde_json::Value) -> serde_json::Value {
578        json
579    }
580
581    #[test]
582    fn elem_match_operator_on_scalar_array() {
583        let d = doc(serde_json::json!({"scores": [50, 85, 60]}));
584        // Operator expression applied directly to each element.
585        assert!(matches_selector(
586            &d,
587            &serde_json::json!({"scores": {"$elemMatch": {"$gt": 80}}})
588        ));
589        assert!(!matches_selector(
590            &d,
591            &serde_json::json!({"scores": {"$elemMatch": {"$gt": 90}}})
592        ));
593        // Bare scalar operand -> implicit $eq against each element.
594        assert!(matches_selector(
595            &d,
596            &serde_json::json!({"scores": {"$elemMatch": 85}})
597        ));
598    }
599
600    #[test]
601    fn elem_match_subdocument_array() {
602        let d = doc(serde_json::json!({
603            "items": [{"subject": "math", "score": 90}, {"subject": "art", "score": 70}]
604        }));
605        assert!(matches_selector(
606            &d,
607            &serde_json::json!({"items": {"$elemMatch": {"subject": "math"}}})
608        ));
609        assert!(!matches_selector(
610            &d,
611            &serde_json::json!({"items": {"$elemMatch": {"subject": "history"}}})
612        ));
613    }
614
615    // --- Basic matching ---
616
617    #[test]
618    fn eq_implicit() {
619        let d = doc(serde_json::json!({"name": "Alice", "age": 30}));
620        assert!(matches_selector(&d, &serde_json::json!({"name": "Alice"})));
621        assert!(!matches_selector(&d, &serde_json::json!({"name": "Bob"})));
622    }
623
624    #[test]
625    fn eq_explicit() {
626        let d = doc(serde_json::json!({"age": 30}));
627        assert!(matches_selector(
628            &d,
629            &serde_json::json!({"age": {"$eq": 30}})
630        ));
631    }
632
633    #[test]
634    fn ne() {
635        let d = doc(serde_json::json!({"age": 30}));
636        assert!(matches_selector(
637            &d,
638            &serde_json::json!({"age": {"$ne": 25}})
639        ));
640        assert!(!matches_selector(
641            &d,
642            &serde_json::json!({"age": {"$ne": 30}})
643        ));
644    }
645
646    #[test]
647    fn gt_gte_lt_lte() {
648        let d = doc(serde_json::json!({"age": 30}));
649
650        assert!(matches_selector(
651            &d,
652            &serde_json::json!({"age": {"$gt": 20}})
653        ));
654        assert!(!matches_selector(
655            &d,
656            &serde_json::json!({"age": {"$gt": 30}})
657        ));
658
659        assert!(matches_selector(
660            &d,
661            &serde_json::json!({"age": {"$gte": 30}})
662        ));
663        assert!(!matches_selector(
664            &d,
665            &serde_json::json!({"age": {"$gte": 31}})
666        ));
667
668        assert!(matches_selector(
669            &d,
670            &serde_json::json!({"age": {"$lt": 40}})
671        ));
672        assert!(!matches_selector(
673            &d,
674            &serde_json::json!({"age": {"$lt": 30}})
675        ));
676
677        assert!(matches_selector(
678            &d,
679            &serde_json::json!({"age": {"$lte": 30}})
680        ));
681        assert!(!matches_selector(
682            &d,
683            &serde_json::json!({"age": {"$lte": 29}})
684        ));
685    }
686
687    #[test]
688    fn in_nin() {
689        let d = doc(serde_json::json!({"color": "red"}));
690
691        assert!(matches_selector(
692            &d,
693            &serde_json::json!({"color": {"$in": ["red", "blue"]}})
694        ));
695        assert!(!matches_selector(
696            &d,
697            &serde_json::json!({"color": {"$in": ["green", "blue"]}})
698        ));
699
700        assert!(matches_selector(
701            &d,
702            &serde_json::json!({"color": {"$nin": ["green", "blue"]}})
703        ));
704        assert!(!matches_selector(
705            &d,
706            &serde_json::json!({"color": {"$nin": ["red", "blue"]}})
707        ));
708    }
709
710    #[test]
711    fn exists() {
712        let d = doc(serde_json::json!({"name": "Alice"}));
713
714        assert!(matches_selector(
715            &d,
716            &serde_json::json!({"name": {"$exists": true}})
717        ));
718        assert!(!matches_selector(
719            &d,
720            &serde_json::json!({"age": {"$exists": true}})
721        ));
722        assert!(matches_selector(
723            &d,
724            &serde_json::json!({"age": {"$exists": false}})
725        ));
726    }
727
728    #[test]
729    fn type_check() {
730        let d = doc(serde_json::json!({"name": "Alice", "age": 30, "active": true}));
731
732        assert!(matches_selector(
733            &d,
734            &serde_json::json!({"name": {"$type": "string"}})
735        ));
736        assert!(matches_selector(
737            &d,
738            &serde_json::json!({"age": {"$type": "number"}})
739        ));
740        assert!(matches_selector(
741            &d,
742            &serde_json::json!({"active": {"$type": "boolean"}})
743        ));
744    }
745
746    #[test]
747    fn regex_match() {
748        let d = doc(serde_json::json!({"name": "Alice"}));
749
750        assert!(matches_selector(
751            &d,
752            &serde_json::json!({"name": {"$regex": "^Ali"}})
753        ));
754        assert!(!matches_selector(
755            &d,
756            &serde_json::json!({"name": {"$regex": "^Bob"}})
757        ));
758    }
759
760    #[test]
761    fn size_operator() {
762        let d = doc(serde_json::json!({"tags": ["a", "b", "c"]}));
763
764        assert!(matches_selector(
765            &d,
766            &serde_json::json!({"tags": {"$size": 3}})
767        ));
768        assert!(!matches_selector(
769            &d,
770            &serde_json::json!({"tags": {"$size": 2}})
771        ));
772    }
773
774    #[test]
775    fn all_operator() {
776        let d = doc(serde_json::json!({"tags": ["a", "b", "c"]}));
777
778        assert!(matches_selector(
779            &d,
780            &serde_json::json!({"tags": {"$all": ["a", "c"]}})
781        ));
782        assert!(!matches_selector(
783            &d,
784            &serde_json::json!({"tags": {"$all": ["a", "d"]}})
785        ));
786    }
787
788    #[test]
789    fn elem_match() {
790        let d = doc(serde_json::json!({
791            "scores": [
792                {"subject": "math", "grade": 90},
793                {"subject": "english", "grade": 75}
794            ]
795        }));
796
797        assert!(matches_selector(
798            &d,
799            &serde_json::json!({"scores": {"$elemMatch": {"subject": "math", "grade": {"$gt": 80}}}})
800        ));
801        assert!(!matches_selector(
802            &d,
803            &serde_json::json!({"scores": {"$elemMatch": {"subject": "math", "grade": {"$gt": 95}}}})
804        ));
805    }
806
807    #[test]
808    fn mod_operator() {
809        let d = doc(serde_json::json!({"n": 10}));
810
811        assert!(matches_selector(
812            &d,
813            &serde_json::json!({"n": {"$mod": [3, 1]}})
814        ));
815        assert!(!matches_selector(
816            &d,
817            &serde_json::json!({"n": {"$mod": [3, 0]}})
818        ));
819    }
820
821    // --- Logical operators ---
822
823    #[test]
824    fn and_operator() {
825        let d = doc(serde_json::json!({"age": 30, "active": true}));
826
827        assert!(matches_selector(
828            &d,
829            &serde_json::json!({"$and": [{"age": {"$gte": 20}}, {"active": true}]})
830        ));
831        assert!(!matches_selector(
832            &d,
833            &serde_json::json!({"$and": [{"age": {"$gte": 20}}, {"active": false}]})
834        ));
835    }
836
837    #[test]
838    fn or_operator() {
839        let d = doc(serde_json::json!({"age": 30}));
840
841        assert!(matches_selector(
842            &d,
843            &serde_json::json!({"$or": [{"age": 30}, {"age": 40}]})
844        ));
845        assert!(!matches_selector(
846            &d,
847            &serde_json::json!({"$or": [{"age": 20}, {"age": 40}]})
848        ));
849    }
850
851    #[test]
852    fn not_operator() {
853        let d = doc(serde_json::json!({"age": 30}));
854
855        assert!(matches_selector(
856            &d,
857            &serde_json::json!({"$not": {"age": 40}})
858        ));
859        assert!(!matches_selector(
860            &d,
861            &serde_json::json!({"$not": {"age": 30}})
862        ));
863    }
864
865    #[test]
866    fn nor_operator() {
867        let d = doc(serde_json::json!({"age": 30}));
868
869        assert!(matches_selector(
870            &d,
871            &serde_json::json!({"$nor": [{"age": 20}, {"age": 40}]})
872        ));
873        assert!(!matches_selector(
874            &d,
875            &serde_json::json!({"$nor": [{"age": 30}, {"age": 40}]})
876        ));
877    }
878
879    // --- Nested fields ---
880
881    #[test]
882    fn nested_field_access() {
883        let d = doc(serde_json::json!({"address": {"city": "NYC", "zip": "10001"}}));
884
885        assert!(matches_selector(
886            &d,
887            &serde_json::json!({"address.city": "NYC"})
888        ));
889        assert!(!matches_selector(
890            &d,
891            &serde_json::json!({"address.city": "LA"})
892        ));
893    }
894
895    // --- Multiple conditions ---
896
897    #[test]
898    fn multiple_field_conditions() {
899        let d = doc(serde_json::json!({"name": "Alice", "age": 30}));
900
901        // Both must match (implicit AND)
902        assert!(matches_selector(
903            &d,
904            &serde_json::json!({"name": "Alice", "age": {"$gte": 25}})
905        ));
906        assert!(!matches_selector(
907            &d,
908            &serde_json::json!({"name": "Alice", "age": {"$gte": 35}})
909        ));
910    }
911
912    #[test]
913    fn combined_operators_on_field() {
914        let d = doc(serde_json::json!({"age": 30}));
915
916        // Range: 20 < age < 40
917        assert!(matches_selector(
918            &d,
919            &serde_json::json!({"age": {"$gt": 20, "$lt": 40}})
920        ));
921        assert!(!matches_selector(
922            &d,
923            &serde_json::json!({"age": {"$gt": 30, "$lt": 40}})
924        ));
925    }
926
927    // --- Projection ---
928
929    #[test]
930    fn project_fields() {
931        let d = serde_json::json!({"_id": "doc1", "_rev": "1-abc", "name": "Alice", "age": 30});
932        let projected = project(d, &["name".to_string()]);
933
934        assert_eq!(projected["_id"], "doc1");
935        assert_eq!(projected["name"], "Alice");
936        assert!(projected.get("age").is_none());
937    }
938
939    // --- Missing fields ---
940
941    #[test]
942    fn missing_field_ne_matches() {
943        // $ne on missing field should match (field != value is true when field doesn't exist)
944        let d = doc(serde_json::json!({"name": "Alice"}));
945        assert!(matches_selector(
946            &d,
947            &serde_json::json!({"age": {"$ne": 30}})
948        ));
949    }
950
951    #[test]
952    fn missing_field_eq_fails() {
953        let d = doc(serde_json::json!({"name": "Alice"}));
954        assert!(!matches_selector(
955            &d,
956            &serde_json::json!({"age": {"$eq": 30}})
957        ));
958    }
959}