Skip to main content

moltendb_core/
query.rs

1// ─── query.rs ─────────────────────────────────────────────────────────────────
2// This file implements the query engine — the logic that filters, projects,
3// and evaluates conditions on JSON documents.
4//
5// Three main capabilities:
6//
7//   1. Field projection (project / exclude)
8//      Select only specific fields from a document, or exclude specific fields.
9//      Both support dot-notation for nested fields (e.g. "meta.logins").
10//
11//   2. Nested value access (get_nested_value / insert_nested_value)
12//      Helper functions to read or write a value at any depth in a JSON object
13//      using a dot-separated path like "meta.shopping_cart.active_order".
14//
15//   3. WHERE clause evaluation (evaluate_where)
16//      Evaluate a MongoDB-style query object against a document.
17//      Supports: $eq, $ne, $gt, $gte, $lt, $lte, $contains, $or, $and, and implicit equality.
18// ─────────────────────────────────────────────────────────────────────────────
19
20use crate::engine::DbError;
21use serde_json::{Value, Map};
22
23/// Select only the specified fields from a document (field projection).
24///
25/// This is similar to SQL's SELECT col1, col2 or GraphQL's field selection.
26/// Fields are specified as dot-notation strings (e.g. "meta.logins").
27/// Nested fields are reconstructed in the output — selecting "meta.logins"
28/// from { name: "Alice", meta: { logins: 10, role: "admin" } } produces
29/// { meta: { logins: 10 } } — only the requested nested field, not the whole
30/// parent object.
31///
32/// Fields that don't exist in the document are silently skipped.
33pub fn project(doc: &Value, fields: &[Value]) -> Value {
34    // Start with an empty output object.
35    let mut filtered_doc = Map::new();
36
37    for field in fields {
38        // Each field must be a string (e.g. "name" or "meta.logins").
39        if let Some(field_path) = field.as_str() {
40            // Split the dot-notation path into parts: "meta.logins" → ["meta", "logins"]
41            let parts: Vec<&str> = field_path.split('.').collect();
42
43            // Try to read the value at this path from the source document.
44            if let Some(val) = get_nested_value(doc, &parts) {
45                // Write the value into the output document at the same path,
46                // creating intermediate objects as needed.
47                insert_nested_value(&mut filtered_doc, &parts, val);
48            }
49        }
50    }
51    Value::Object(filtered_doc)
52}
53
54/// Read a value at any depth from a JSON object using a dot-notation path.
55///
56/// `parts` is the path split by '.': ["meta", "logins"] reads doc.meta.logins.
57///
58/// Returns `Some(value)` if the full path exists, `None` if any part is missing.
59/// The returned value is cloned — the caller owns it.
60pub fn get_nested_value(doc: &Value, parts: &[&str]) -> Option<Value> {
61    let mut current = doc;
62    for part in parts {
63        // Try to descend one level. If the key doesn't exist, return None.
64        if let Some(v) = current.get(*part) {
65            current = v;
66        } else {
67            return None; // Path doesn't exist in this document
68        }
69    }
70    // Clone the final value so the caller owns it independently of the document.
71    Some(current.clone())
72}
73
74/// Write a value at any depth into a JSON object, creating intermediate
75/// objects as needed.
76///
77/// Example: insert_nested_value(map, ["meta", "logins"], 10)
78///   → map becomes { "meta": { "logins": 10 } }
79///   If "meta" already exists as an object, "logins" is added to it.
80///   If "meta" doesn't exist, it is created as a new empty object first.
81fn insert_nested_value(target: &mut Map<String, Value>, parts: &[&str], value: Value) {
82    // Base case: empty path — nothing to insert.
83    if parts.is_empty() { return; }
84
85    let key = parts[0].to_string();
86
87    if parts.len() == 1 {
88        // Base case: we're at the final key — insert the value directly.
89        target.insert(key, value);
90    } else {
91        // Recursive case: we need to go deeper.
92        // Get or create the intermediate object at `key`.
93        // `entry().or_insert_with()` inserts a new empty object if the key
94        // doesn't exist, then returns a mutable reference to the value.
95        let next_target = target.entry(key).or_insert_with(|| Value::Object(Map::new()));
96
97        // Recurse into the next level of the object.
98        if let Some(next_map) = next_target.as_object_mut() {
99            insert_nested_value(next_map, &parts[1..], value);
100        }
101    }
102}
103
104/// Remove specified fields from a document (the inverse of project).
105///
106/// Returns a copy of the document with the listed fields removed.
107/// Supports dot-notation for nested fields (e.g. "meta.logins" removes only
108/// the `logins` field inside `meta`, leaving other fields in `meta` intact).
109///
110/// If removing a nested field leaves its parent object empty, the parent
111/// object is also removed (e.g. removing "meta.logins" from { meta: { logins: 10 } }
112/// removes the entire `meta` key since it would be empty).
113///
114/// Note: `fields` and `excludedFields` are mutually exclusive — the handler
115/// validates this before calling either function.
116pub fn exclude(doc: &Value, fields: &[Value]) -> Value {
117    // Clone the document into a mutable Map so we can remove fields from it.
118    let mut result = match doc.as_object() {
119        Some(obj) => obj.clone(),
120        // If the document isn't an object (e.g. it's a string or number),
121        // return it unchanged — nothing to exclude.
122        None => return doc.clone(),
123    };
124
125    for field in fields {
126        if let Some(field_path) = field.as_str() {
127            // Split the dot-notation path into parts.
128            let parts: Vec<&str> = field_path.split('.').collect();
129            remove_nested_value(&mut result, &parts);
130        }
131    }
132    Value::Object(result)
133}
134
135/// Remove a value at any depth from a JSON object, leaving sibling keys intact.
136///
137/// After removing a nested field, if the parent object is now empty, the
138/// parent is also removed. This prevents leaving empty `{}` objects behind.
139fn remove_nested_value(target: &mut Map<String, Value>, parts: &[&str]) {
140    // Base case: empty path — nothing to remove.
141    if parts.is_empty() { return; }
142
143    let key = parts[0];
144
145    if parts.len() == 1 {
146        // Base case: remove the key directly from this object.
147        target.remove(key);
148    } else if let Some(child) = target.get_mut(key) {
149        // Recursive case: descend into the child object and remove from there.
150        if let Some(child_map) = child.as_object_mut() {
151            remove_nested_value(child_map, &parts[1..]);
152        }
153        // After recursing, check if the child object is now empty.
154        // If so, remove the parent key too — no point keeping an empty {}.
155        if target.get(key).and_then(|v| v.as_object()).map(|o| o.is_empty()).unwrap_or(false) {
156            target.remove(key);
157        }
158    }
159}
160
161/// Evaluate a MongoDB-style WHERE clause against a single document.
162///
163/// Returns `true` if the document matches all conditions, `false` otherwise.
164///
165/// The query object is a JSON object where each key is either:
166///   - A field name (possibly dot-notation): { "role": "admin" }
167///   - A logical operator: { "$or": [...], "$and": [...] }
168///
169/// Field conditions can be:
170///   - An implicit equality: { "role": "admin" } (shorthand for $eq)
171///   - An operator object: { "age": { "$gt": 18 } }
172///
173/// Supported operators:
174///   Equality:   $eq / $equals,  $ne / $notEquals
175///   Numeric:    $gt / $greaterThan,  $gte,  $lt / $lessThan,  $lte
176///   String:     $contains / $ct
177///   Logical:    $or (array of sub-queries, any must match)
178///               $and (array of sub-queries, all must match)
179pub fn evaluate_where(doc: &Value, query: &Value) -> Result<bool, DbError> {
180    // If the query is not an object (e.g. null or a string), it passes automatically.
181    let query_obj = match query.as_object() {
182        Some(obj) => obj,
183        // No conditions = everything matches
184        None => return Ok(true),
185    };
186
187    // Every condition in the query object must pass (implicit AND at the top level).
188    for (key, condition) in query_obj {
189
190        // ── Logical operators ($or / $and) ────────────────────────────────────
191        if key == "$or" {
192            let sub_queries = match condition.as_array() {
193                Some(arr) => arr,
194                None => return Err(DbError::InvalidQuery("$or expects an array".to_string())),
195            };
196            let mut any_passed = false;
197            for sub in sub_queries {
198                if evaluate_where(doc, sub)? {
199                    any_passed = true;
200                    break;
201                }
202            }
203            if !any_passed { return Ok(false); }
204            continue;
205        }
206
207        if key == "$and" {
208            let sub_queries = match condition.as_array() {
209                Some(arr) => arr,
210                None => return Err(DbError::InvalidQuery("$and expects an array".to_string())),
211            };
212            for sub in sub_queries {
213                if !evaluate_where(doc, sub)? { return Ok(false); }
214            }
215            continue;
216        }
217
218        // ── Field condition ───────────────────────────────────────────────────
219        // The key is a field name (possibly dot-notation like "meta.logins").
220        // Split it into path parts and read the value from the document.
221        let parts: Vec<&str> = key.split('.').collect();
222        let doc_val_opt = get_nested_value(doc, &parts);
223
224        // ── Implicit equality ─────────────────────────────────────────────────
225        // { "role": "admin" } is shorthand for { "role": { "$eq": "admin" } }
226        if !condition.is_object() {
227            if let Some(dv) = &doc_val_opt {
228                // The document's field value must equal the condition value.
229                // String comparisons are case-insensitive.
230                let matches = match (dv, condition) {
231                    (Value::String(a), Value::String(b)) => a.to_lowercase() == b.to_lowercase(),
232                    _ => dv == condition,
233                };
234                if !matches { return Ok(false); }
235            } else {
236                // The field doesn't exist in the document — condition fails.
237                return Ok(false);
238            }
239            continue;
240        }
241
242        // ── Operator matching ─────────────────────────────────────────────────
243        // { "age": { "$gt": 18, "$lt": 65 } }
244        // All operators in the condition object must pass.
245        let cond_obj = condition.as_object().ok_or_else(|| {
246            DbError::InvalidQuery(format!("Field condition for '{}' must be an object or plain value", key))
247        })?;
248        // Use Null as the document value if the field doesn't exist.
249        let doc_val_ref = doc_val_opt.as_ref().unwrap_or(&Value::Null);
250
251        for (op, op_val) in cond_obj {
252            let passed: bool = match op.as_str() {
253                // ── Equality operators ────────────────────────────────────────
254                // Both shorthand ($eq) and verbose ($equals) are supported.
255                "$eq" | "$equals" => match (doc_val_ref, op_val) {
256                    (Value::String(a), Value::String(b)) => a.to_lowercase() == b.to_lowercase(),
257                    _ => doc_val_ref == op_val,
258                },
259                "$ne" | "$notEquals" => match (doc_val_ref, op_val) {
260                    (Value::String(a), Value::String(b)) => a.to_lowercase() != b.to_lowercase(),
261                    _ => doc_val_ref != op_val,
262                },
263
264
265                // ── Numeric comparison operators ──────────────────────────────
266                // Both shorthand ($gt) and verbose ($greaterThan) are supported.
267                // as_f64() converts any JSON number to a 64-bit float for comparison.
268                "$gt" | "$greaterThan" | "$gte" | "$lt" | "$lessThan" | "$lte" => {
269                    if let (Some(d_num), Some(o_num)) = (doc_val_ref.as_f64(), op_val.as_f64()) {
270                        match op.as_str() {
271                            "$gt" | "$greaterThan" => d_num > o_num,
272                            "$gte"                 => d_num >= o_num,
273                            "$lt" | "$lessThan"    => d_num < o_num,
274                            "$lte"                 => d_num <= o_num,
275                            _ => false,
276                        }
277                    } else {
278                        // One of the values is not a number — condition fails.
279                        false
280                    }
281                },
282
283
284                // ── Contains operator ─────────────────────────────────────────
285                // Works on both strings (substring check) AND arrays (membership check).
286                "$contains" | "$ct" => {
287                    match doc_val_ref {
288                        // String case: check if the document string contains the needle string.
289                        Value::String(d_str) => {
290                            if let Some(o_str) = op_val.as_str() {
291                                d_str.to_lowercase().contains(&o_str.to_lowercase())
292                            } else {
293                                false
294                            }
295                        }
296                        // Array case: check if any element in the array equals the target value.
297                        Value::Array(arr) => arr.contains(&op_val),
298                        // Any other type (number, object, null) — condition fails.
299                        _ => false,
300                    }
301                },
302
303
304
305                // ── In operator ───────────────────────────────────────────────────
306                // Both shorthand ($in) and verbose ($oneOf) are supported.
307                "$in" | "$oneOf" => {
308                    if let Some(allowed) = op_val.as_array() {
309                        // Check if the document value matches any element in the list.
310                        allowed.iter().any(|v| match (doc_val_ref, v) {
311                            (Value::String(a), Value::String(b)) => a.to_lowercase() == b.to_lowercase(),
312                            _ => doc_val_ref == v,
313                        })
314                    } else {
315                        // The operator value is not an array — fail.
316                        return Err(DbError::InvalidQuery(format!("{} expects an array", op)));
317                    }
318                },
319
320                // ── Not-in operator ───────────────────────────────────────────────────
321                // Both shorthand ($nin) and verbose ($notIn) are supported.
322                "$nin" | "$notIn" => {
323                    if let Some(excluded) = op_val.as_array() {
324                        // True only if the document value is NOT in the exclusion list.
325                        !excluded.iter().any(|v| match (doc_val_ref, v) {
326                            (Value::String(a), Value::String(b)) => a.to_lowercase() == b.to_lowercase(),
327                            _ => doc_val_ref == v,
328                        })
329                    } else {
330                        // The operator value is not an array — fail.
331                        return Err(DbError::InvalidQuery(format!("{} expects an array", op)));
332                    }
333                },
334
335                // Unknown operator — fail the condition rather than silently passing.
336                _ => return Err(DbError::InvalidQuery(format!("Unknown operator: {}", op))),
337            };
338
339            // If any operator in the condition object fails, the whole condition fails.
340            if !passed { return Ok(false); }
341        }
342    }
343
344    // All conditions passed.
345    Ok(true)
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use serde_json::json;
352
353    #[test]
354    fn test_evaluate_where_basic() {
355        let doc = json!({ "name": "Alice", "age": 30 });
356        
357        // Simple equality
358        assert!(evaluate_where(&doc, &json!({ "name": "Alice" })).unwrap());
359        assert!(!evaluate_where(&doc, &json!({ "name": "Bob" })).unwrap());
360        
361        // Explicit $eq
362        assert!(evaluate_where(&doc, &json!({ "name": { "$eq": "Alice" } })).unwrap());
363        
364        // Case-insensitivity for strings
365        assert!(evaluate_where(&doc, &json!({ "name": "alice" })).unwrap());
366    }
367
368    #[test]
369    fn test_evaluate_where_numeric() {
370        let doc = json!({ "age": 30 });
371        
372        assert!(evaluate_where(&doc, &json!({ "age": { "$gt": 20 } })).unwrap());
373        assert!(evaluate_where(&doc, &json!({ "age": { "$gte": 30 } })).unwrap());
374        assert!(evaluate_where(&doc, &json!({ "age": { "$lt": 40 } })).unwrap());
375        assert!(evaluate_where(&doc, &json!({ "age": { "$lte": 30 } })).unwrap());
376        
377        assert!(!evaluate_where(&doc, &json!({ "age": { "$gt": 30 } })).unwrap());
378    }
379
380    #[test]
381    fn test_evaluate_where_invalid_ops() {
382        let doc = json!({ "name": "Alice" });
383        
384        let res = evaluate_where(&doc, &json!({ "name": { "$invalid": "val" } }));
385        assert!(res.is_err());
386        if let Err(DbError::InvalidQuery(msg)) = res {
387            assert!(msg.contains("Unknown operator"));
388        } else {
389            panic!("Expected InvalidQuery error");
390        }
391    }
392
393    #[test]
394    fn test_evaluate_where_logical() {
395        let doc = json!({ "name": "Alice", "age": 30 });
396        
397        // $or
398        assert!(evaluate_where(&doc, &json!({ "$or": [{ "name": "Alice" }, { "name": "Bob" }] })).unwrap());
399        assert!(evaluate_where(&doc, &json!({ "$or": [{ "name": "Bob" }, { "age": 30 }] })).unwrap());
400        assert!(!evaluate_where(&doc, &json!({ "$or": [{ "name": "Bob" }, { "age": 20 }] })).unwrap());
401        
402        // $and
403        assert!(evaluate_where(&doc, &json!({ "$and": [{ "name": "Alice" }, { "age": 30 }] })).unwrap());
404        assert!(!evaluate_where(&doc, &json!({ "$and": [{ "name": "Alice" }, { "age": 20 }] })).unwrap());
405    }
406
407    #[test]
408    fn test_evaluate_where_in_nin() {
409        let doc = json!({ "role": "admin" });
410        
411        assert!(evaluate_where(&doc, &json!({ "role": { "$in": ["admin", "user"] } })).unwrap());
412        assert!(!evaluate_where(&doc, &json!({ "role": { "$in": ["guest", "user"] } })).unwrap());
413        
414        assert!(evaluate_where(&doc, &json!({ "role": { "$nin": ["guest", "user"] } })).unwrap());
415        assert!(!evaluate_where(&doc, &json!({ "role": { "$nin": ["admin", "user"] } })).unwrap());
416    }
417}