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}