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
345fn match_operator(
346    field_value: Option<&serde_json::Value>,
347    op: &str,
348    operand: &serde_json::Value,
349) -> bool {
350    match op {
351        "$eq" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Equal),
352        "$ne" => field_value.is_none_or(|v| collate(v, operand) != std::cmp::Ordering::Equal),
353        "$gt" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Greater),
354        "$gte" => field_value.is_some_and(|v| collate(v, operand) != std::cmp::Ordering::Less),
355        "$lt" => field_value.is_some_and(|v| collate(v, operand) == std::cmp::Ordering::Less),
356        "$lte" => field_value.is_some_and(|v| collate(v, operand) != std::cmp::Ordering::Greater),
357        "$in" => {
358            if let Some(arr) = operand.as_array() {
359                field_value.is_some_and(|v| {
360                    arr.iter()
361                        .any(|item| collate(v, item) == std::cmp::Ordering::Equal)
362                })
363            } else {
364                false
365            }
366        }
367        "$nin" => {
368            if let Some(arr) = operand.as_array() {
369                field_value.is_none_or(|v| {
370                    !arr.iter()
371                        .any(|item| collate(v, item) == std::cmp::Ordering::Equal)
372                })
373            } else {
374                true
375            }
376        }
377        "$exists" => {
378            let should_exist = operand.as_bool().unwrap_or(true);
379            if should_exist {
380                field_value.is_some()
381            } else {
382                field_value.is_none()
383            }
384        }
385        "$type" => {
386            if let Some(type_name) = operand.as_str() {
387                field_value.is_some_and(|v| json_type_name(v) == type_name)
388            } else {
389                false
390            }
391        }
392        "$regex" => {
393            if let Some(pattern) = operand.as_str() {
394                field_value.is_some_and(|v| {
395                    if let Some(s) = v.as_str() {
396                        Regex::new(pattern).is_ok_and(|re| re.is_match(s))
397                    } else {
398                        false
399                    }
400                })
401            } else {
402                false
403            }
404        }
405        "$size" => {
406            if let Some(expected_size) = operand.as_u64() {
407                field_value.is_some_and(|v| {
408                    v.as_array()
409                        .is_some_and(|arr| arr.len() as u64 == expected_size)
410                })
411            } else {
412                false
413            }
414        }
415        "$all" => {
416            if let Some(required) = operand.as_array() {
417                field_value.is_some_and(|v| {
418                    if let Some(arr) = v.as_array() {
419                        required.iter().all(|req| {
420                            arr.iter()
421                                .any(|item| collate(item, req) == std::cmp::Ordering::Equal)
422                        })
423                    } else {
424                        false
425                    }
426                })
427            } else {
428                false
429            }
430        }
431        "$elemMatch" => field_value.is_some_and(|v| {
432            if let Some(arr) = v.as_array() {
433                arr.iter().any(|elem| matches_selector(elem, operand))
434            } else {
435                false
436            }
437        }),
438        "$not" => {
439            // Field-level $not: negate the sub-condition applied to this field's value
440            if let Some(ops) = operand.as_object() {
441                for (sub_op, sub_operand) in ops {
442                    if match_operator(field_value, sub_op, sub_operand) {
443                        return false;
444                    }
445                }
446                true
447            } else {
448                // Implicit $eq negation
449                !match_operator(field_value, "$eq", operand)
450            }
451        }
452        "$mod" => {
453            if let Some(arr) = operand.as_array() {
454                if arr.len() == 2 {
455                    let divisor = arr[0].as_i64();
456                    let remainder = arr[1].as_i64();
457                    if let (Some(d), Some(r)) = (divisor, remainder) {
458                        field_value
459                            .is_some_and(|v| v.as_i64().is_some_and(|n| d != 0 && n % d == r))
460                    } else {
461                        false
462                    }
463                } else {
464                    false
465                }
466            } else {
467                false
468            }
469        }
470        _ => false,
471    }
472}
473
474fn match_and(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
475    if let Some(arr) = condition.as_array() {
476        arr.iter().all(|sub| matches_selector(doc, sub))
477    } else {
478        false
479    }
480}
481
482fn match_or(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
483    if let Some(arr) = condition.as_array() {
484        arr.iter().any(|sub| matches_selector(doc, sub))
485    } else {
486        false
487    }
488}
489
490fn match_not(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
491    !matches_selector(doc, condition)
492}
493
494fn match_nor(doc: &serde_json::Value, condition: &serde_json::Value) -> bool {
495    if let Some(arr) = condition.as_array() {
496        !arr.iter().any(|sub| matches_selector(doc, sub))
497    } else {
498        false
499    }
500}
501
502/// Get a nested field from a JSON value using dot notation.
503fn get_nested_field<'a>(doc: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
504    let mut current = doc;
505    for part in path.split('.') {
506        match current.get(part) {
507            Some(v) => current = v,
508            None => return None,
509        }
510    }
511    Some(current)
512}
513
514/// Return the CouchDB type name for a JSON value.
515fn json_type_name(value: &serde_json::Value) -> &'static str {
516    match value {
517        serde_json::Value::Null => "null",
518        serde_json::Value::Bool(_) => "boolean",
519        serde_json::Value::Number(_) => "number",
520        serde_json::Value::String(_) => "string",
521        serde_json::Value::Array(_) => "array",
522        serde_json::Value::Object(_) => "object",
523    }
524}
525
526/// Project a document to only include the specified fields.
527fn project(doc: serde_json::Value, fields: &[String]) -> serde_json::Value {
528    let mut result = serde_json::Map::new();
529
530    if let serde_json::Value::Object(map) = &doc {
531        for field in fields {
532            // Always include _id and _rev
533            if let Some(val) = map.get(field) {
534                result.insert(field.clone(), val.clone());
535            }
536        }
537        // Always include _id
538        if let Some(id) = map.get("_id") {
539            result
540                .entry("_id".to_string())
541                .or_insert_with(|| id.clone());
542        }
543    }
544
545    serde_json::Value::Object(result)
546}
547
548// ---------------------------------------------------------------------------
549// Tests
550// ---------------------------------------------------------------------------
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    fn doc(json: serde_json::Value) -> serde_json::Value {
557        json
558    }
559
560    // --- Basic matching ---
561
562    #[test]
563    fn eq_implicit() {
564        let d = doc(serde_json::json!({"name": "Alice", "age": 30}));
565        assert!(matches_selector(&d, &serde_json::json!({"name": "Alice"})));
566        assert!(!matches_selector(&d, &serde_json::json!({"name": "Bob"})));
567    }
568
569    #[test]
570    fn eq_explicit() {
571        let d = doc(serde_json::json!({"age": 30}));
572        assert!(matches_selector(
573            &d,
574            &serde_json::json!({"age": {"$eq": 30}})
575        ));
576    }
577
578    #[test]
579    fn ne() {
580        let d = doc(serde_json::json!({"age": 30}));
581        assert!(matches_selector(
582            &d,
583            &serde_json::json!({"age": {"$ne": 25}})
584        ));
585        assert!(!matches_selector(
586            &d,
587            &serde_json::json!({"age": {"$ne": 30}})
588        ));
589    }
590
591    #[test]
592    fn gt_gte_lt_lte() {
593        let d = doc(serde_json::json!({"age": 30}));
594
595        assert!(matches_selector(
596            &d,
597            &serde_json::json!({"age": {"$gt": 20}})
598        ));
599        assert!(!matches_selector(
600            &d,
601            &serde_json::json!({"age": {"$gt": 30}})
602        ));
603
604        assert!(matches_selector(
605            &d,
606            &serde_json::json!({"age": {"$gte": 30}})
607        ));
608        assert!(!matches_selector(
609            &d,
610            &serde_json::json!({"age": {"$gte": 31}})
611        ));
612
613        assert!(matches_selector(
614            &d,
615            &serde_json::json!({"age": {"$lt": 40}})
616        ));
617        assert!(!matches_selector(
618            &d,
619            &serde_json::json!({"age": {"$lt": 30}})
620        ));
621
622        assert!(matches_selector(
623            &d,
624            &serde_json::json!({"age": {"$lte": 30}})
625        ));
626        assert!(!matches_selector(
627            &d,
628            &serde_json::json!({"age": {"$lte": 29}})
629        ));
630    }
631
632    #[test]
633    fn in_nin() {
634        let d = doc(serde_json::json!({"color": "red"}));
635
636        assert!(matches_selector(
637            &d,
638            &serde_json::json!({"color": {"$in": ["red", "blue"]}})
639        ));
640        assert!(!matches_selector(
641            &d,
642            &serde_json::json!({"color": {"$in": ["green", "blue"]}})
643        ));
644
645        assert!(matches_selector(
646            &d,
647            &serde_json::json!({"color": {"$nin": ["green", "blue"]}})
648        ));
649        assert!(!matches_selector(
650            &d,
651            &serde_json::json!({"color": {"$nin": ["red", "blue"]}})
652        ));
653    }
654
655    #[test]
656    fn exists() {
657        let d = doc(serde_json::json!({"name": "Alice"}));
658
659        assert!(matches_selector(
660            &d,
661            &serde_json::json!({"name": {"$exists": true}})
662        ));
663        assert!(!matches_selector(
664            &d,
665            &serde_json::json!({"age": {"$exists": true}})
666        ));
667        assert!(matches_selector(
668            &d,
669            &serde_json::json!({"age": {"$exists": false}})
670        ));
671    }
672
673    #[test]
674    fn type_check() {
675        let d = doc(serde_json::json!({"name": "Alice", "age": 30, "active": true}));
676
677        assert!(matches_selector(
678            &d,
679            &serde_json::json!({"name": {"$type": "string"}})
680        ));
681        assert!(matches_selector(
682            &d,
683            &serde_json::json!({"age": {"$type": "number"}})
684        ));
685        assert!(matches_selector(
686            &d,
687            &serde_json::json!({"active": {"$type": "boolean"}})
688        ));
689    }
690
691    #[test]
692    fn regex_match() {
693        let d = doc(serde_json::json!({"name": "Alice"}));
694
695        assert!(matches_selector(
696            &d,
697            &serde_json::json!({"name": {"$regex": "^Ali"}})
698        ));
699        assert!(!matches_selector(
700            &d,
701            &serde_json::json!({"name": {"$regex": "^Bob"}})
702        ));
703    }
704
705    #[test]
706    fn size_operator() {
707        let d = doc(serde_json::json!({"tags": ["a", "b", "c"]}));
708
709        assert!(matches_selector(
710            &d,
711            &serde_json::json!({"tags": {"$size": 3}})
712        ));
713        assert!(!matches_selector(
714            &d,
715            &serde_json::json!({"tags": {"$size": 2}})
716        ));
717    }
718
719    #[test]
720    fn all_operator() {
721        let d = doc(serde_json::json!({"tags": ["a", "b", "c"]}));
722
723        assert!(matches_selector(
724            &d,
725            &serde_json::json!({"tags": {"$all": ["a", "c"]}})
726        ));
727        assert!(!matches_selector(
728            &d,
729            &serde_json::json!({"tags": {"$all": ["a", "d"]}})
730        ));
731    }
732
733    #[test]
734    fn elem_match() {
735        let d = doc(serde_json::json!({
736            "scores": [
737                {"subject": "math", "grade": 90},
738                {"subject": "english", "grade": 75}
739            ]
740        }));
741
742        assert!(matches_selector(
743            &d,
744            &serde_json::json!({"scores": {"$elemMatch": {"subject": "math", "grade": {"$gt": 80}}}})
745        ));
746        assert!(!matches_selector(
747            &d,
748            &serde_json::json!({"scores": {"$elemMatch": {"subject": "math", "grade": {"$gt": 95}}}})
749        ));
750    }
751
752    #[test]
753    fn mod_operator() {
754        let d = doc(serde_json::json!({"n": 10}));
755
756        assert!(matches_selector(
757            &d,
758            &serde_json::json!({"n": {"$mod": [3, 1]}})
759        ));
760        assert!(!matches_selector(
761            &d,
762            &serde_json::json!({"n": {"$mod": [3, 0]}})
763        ));
764    }
765
766    // --- Logical operators ---
767
768    #[test]
769    fn and_operator() {
770        let d = doc(serde_json::json!({"age": 30, "active": true}));
771
772        assert!(matches_selector(
773            &d,
774            &serde_json::json!({"$and": [{"age": {"$gte": 20}}, {"active": true}]})
775        ));
776        assert!(!matches_selector(
777            &d,
778            &serde_json::json!({"$and": [{"age": {"$gte": 20}}, {"active": false}]})
779        ));
780    }
781
782    #[test]
783    fn or_operator() {
784        let d = doc(serde_json::json!({"age": 30}));
785
786        assert!(matches_selector(
787            &d,
788            &serde_json::json!({"$or": [{"age": 30}, {"age": 40}]})
789        ));
790        assert!(!matches_selector(
791            &d,
792            &serde_json::json!({"$or": [{"age": 20}, {"age": 40}]})
793        ));
794    }
795
796    #[test]
797    fn not_operator() {
798        let d = doc(serde_json::json!({"age": 30}));
799
800        assert!(matches_selector(
801            &d,
802            &serde_json::json!({"$not": {"age": 40}})
803        ));
804        assert!(!matches_selector(
805            &d,
806            &serde_json::json!({"$not": {"age": 30}})
807        ));
808    }
809
810    #[test]
811    fn nor_operator() {
812        let d = doc(serde_json::json!({"age": 30}));
813
814        assert!(matches_selector(
815            &d,
816            &serde_json::json!({"$nor": [{"age": 20}, {"age": 40}]})
817        ));
818        assert!(!matches_selector(
819            &d,
820            &serde_json::json!({"$nor": [{"age": 30}, {"age": 40}]})
821        ));
822    }
823
824    // --- Nested fields ---
825
826    #[test]
827    fn nested_field_access() {
828        let d = doc(serde_json::json!({"address": {"city": "NYC", "zip": "10001"}}));
829
830        assert!(matches_selector(
831            &d,
832            &serde_json::json!({"address.city": "NYC"})
833        ));
834        assert!(!matches_selector(
835            &d,
836            &serde_json::json!({"address.city": "LA"})
837        ));
838    }
839
840    // --- Multiple conditions ---
841
842    #[test]
843    fn multiple_field_conditions() {
844        let d = doc(serde_json::json!({"name": "Alice", "age": 30}));
845
846        // Both must match (implicit AND)
847        assert!(matches_selector(
848            &d,
849            &serde_json::json!({"name": "Alice", "age": {"$gte": 25}})
850        ));
851        assert!(!matches_selector(
852            &d,
853            &serde_json::json!({"name": "Alice", "age": {"$gte": 35}})
854        ));
855    }
856
857    #[test]
858    fn combined_operators_on_field() {
859        let d = doc(serde_json::json!({"age": 30}));
860
861        // Range: 20 < age < 40
862        assert!(matches_selector(
863            &d,
864            &serde_json::json!({"age": {"$gt": 20, "$lt": 40}})
865        ));
866        assert!(!matches_selector(
867            &d,
868            &serde_json::json!({"age": {"$gt": 30, "$lt": 40}})
869        ));
870    }
871
872    // --- Projection ---
873
874    #[test]
875    fn project_fields() {
876        let d = serde_json::json!({"_id": "doc1", "_rev": "1-abc", "name": "Alice", "age": 30});
877        let projected = project(d, &["name".to_string()]);
878
879        assert_eq!(projected["_id"], "doc1");
880        assert_eq!(projected["name"], "Alice");
881        assert!(projected.get("age").is_none());
882    }
883
884    // --- Missing fields ---
885
886    #[test]
887    fn missing_field_ne_matches() {
888        // $ne on missing field should match (field != value is true when field doesn't exist)
889        let d = doc(serde_json::json!({"name": "Alice"}));
890        assert!(matches_selector(
891            &d,
892            &serde_json::json!({"age": {"$ne": 30}})
893        ));
894    }
895
896    #[test]
897    fn missing_field_eq_fails() {
898        let d = doc(serde_json::json!({"name": "Alice"}));
899        assert!(!matches_selector(
900            &d,
901            &serde_json::json!({"age": {"$eq": 30}})
902        ));
903    }
904}