aurora_db/query.rs
1//! # Aurora Query System
2//!
3//! This module provides a powerful, fluent query interface for filtering, sorting,
4//! and retrieving documents from Aurora collections.
5//!
6//! ## Examples
7//!
8//! ```rust
9//! // Get all active users over 21
10//! let users = db.query("users")
11//! .filter(|f| f.eq("status", "active") && f.gt("age", 21))
12//! .order_by("last_login", false)
13//! .limit(20)
14//! .collect()
15//! .await?;
16//! ```
17
18use crate::Aurora;
19use crate::error::AuroraError;
20use crate::error::Result;
21use crate::types::{Document, Value};
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24
25/// Trait for objects that can filter documents.
26///
27/// This trait is implemented for closures that take a reference to a
28/// `FilterBuilder` and return a boolean, allowing for a natural filter syntax.
29pub trait Queryable {
30 fn matches(&self, doc: &Document) -> bool;
31}
32
33impl<F> Queryable for F
34where
35 F: Fn(&Document) -> bool,
36{
37 fn matches(&self, doc: &Document) -> bool {
38 self(doc)
39 }
40}
41
42/// Builder for creating and executing document queries.
43///
44/// QueryBuilder uses a fluent interface pattern to construct
45/// and execute queries against Aurora collections.
46///
47/// # Examples
48///
49/// ```
50/// // Query for active premium users
51/// let premium_users = db.query("users")
52/// .filter(|f| f.eq("status", "active") && f.eq("account_type", "premium"))
53/// .order_by("created_at", false)
54/// .limit(10)
55/// .collect()
56/// .await?;
57/// ```
58pub struct QueryBuilder<'a> {
59 db: &'a Aurora,
60 collection: String,
61 // CHANGE 1: Add `+ Send` to make the boxed closure thread-safe.
62 filters: Vec<Box<dyn Fn(&Document) -> bool + Send + 'a>>,
63 order_by: Option<(String, bool)>,
64 limit: Option<usize>,
65 offset: Option<usize>,
66 fields: Option<Vec<String>>,
67}
68
69/// Builder for constructing document filter expressions.
70///
71/// This struct provides methods for comparing document fields
72/// with values to create filter conditions.
73///
74/// # Examples
75///
76/// ```
77/// // Combine multiple filter conditions
78/// db.query("products")
79/// .filter(|f| {
80/// f.gte("price", 10.0) &&
81/// f.lte("price", 50.0) &&
82/// f.contains("name", "widget")
83/// })
84/// .collect()
85/// .await?;
86/// ```
87pub struct FilterBuilder<'a, 'b> {
88 doc: &'b Document,
89 _marker: std::marker::PhantomData<&'a ()>,
90}
91
92impl<'a, 'b> FilterBuilder<'a, 'b> {
93 /// Create a new filter builder for the given document
94 pub fn new(doc: &'b Document) -> Self {
95 Self {
96 doc,
97 _marker: std::marker::PhantomData,
98 }
99 }
100
101 /// Check if a field equals a value
102 ///
103 /// # Examples
104 /// ```
105 /// .filter(|f| f.eq("status", "active"))
106 /// ```
107 pub fn eq<T: Into<Value>>(&self, field: &str, value: T) -> bool {
108 let value = value.into();
109 self.doc.data.get(field).map_or(false, |v| v == &value)
110 }
111
112 /// Check if a field is greater than a value
113 ///
114 /// # Examples
115 /// ```
116 /// .filter(|f| f.gt("age", 21))
117 /// ```
118 pub fn gt<T: Into<Value>>(&self, field: &str, value: T) -> bool {
119 let value = value.into();
120 self.doc.data.get(field).map_or(false, |v| v > &value)
121 }
122
123 /// Check if a field is greater than or equal to a value
124 ///
125 /// # Examples
126 /// ```
127 /// .filter(|f| f.gte("age", 21))
128 /// ```
129 pub fn gte<T: Into<Value>>(&self, field: &str, value: T) -> bool {
130 let value = value.into();
131 self.doc.data.get(field).map_or(false, |v| v >= &value)
132 }
133
134 /// Check if a field is less than a value
135 ///
136 /// # Examples
137 /// ```
138 /// .filter(|f| f.lt("age", 65))
139 /// ```
140 pub fn lt<T: Into<Value>>(&self, field: &str, value: T) -> bool {
141 let value = value.into();
142 self.doc.data.get(field).map_or(false, |v| v < &value)
143 }
144
145 /// Check if a field is less than or equal to a value
146 ///
147 /// # Examples
148 /// ```
149 /// .filter(|f| f.lte("age", 65))
150 /// ```
151 pub fn lte<T: Into<Value>>(&self, field: &str, value: T) -> bool {
152 let value = value.into();
153 self.doc.data.get(field).map_or(false, |v| v <= &value)
154 }
155
156 /// Check if a field contains a value
157 ///
158 /// # Examples
159 /// ```
160 /// .filter(|f| f.contains("name", "widget"))
161 /// ```
162 pub fn contains(&self, field: &str, value: &str) -> bool {
163 self.doc.data.get(field).map_or(false, |v| match v {
164 Value::String(s) => s.contains(value),
165 Value::Array(arr) => arr.contains(&Value::String(value.to_string())),
166 _ => false,
167 })
168 }
169
170 /// Check if a field is in a list of values
171 ///
172 /// # Examples
173 /// ```
174 /// .filter(|f| f.in_values("status", &["active", "inactive"]))
175 /// ```
176 pub fn in_values<T: Into<Value> + Clone>(&self, field: &str, values: &[T]) -> bool {
177 let values: Vec<Value> = values.iter().map(|v| v.clone().into()).collect();
178 self.doc
179 .data
180 .get(field)
181 .map_or(false, |v| values.contains(v))
182 }
183
184 /// Check if a field is between two values (inclusive)
185 ///
186 /// # Examples
187 /// ```
188 /// .filter(|f| f.between("age", 18, 65))
189 /// ```
190 pub fn between<T: Into<Value> + Clone>(&self, field: &str, min: T, max: T) -> bool {
191 self.gte(field, min) && self.lte(field, max)
192 }
193
194 /// Check if a field exists and is not null
195 ///
196 /// # Examples
197 /// ```
198 /// .filter(|f| f.exists("email"))
199 /// ```
200 pub fn exists(&self, field: &str) -> bool {
201 self.doc
202 .data
203 .get(field)
204 .map_or(false, |v| !matches!(v, Value::Null))
205 }
206
207 /// Check if a field doesn't exist or is null
208 ///
209 /// # Examples
210 /// ```
211 /// .filter(|f| f.is_null("email"))
212 /// ```
213 pub fn is_null(&self, field: &str) -> bool {
214 self.doc
215 .data
216 .get(field)
217 .map_or(true, |v| matches!(v, Value::Null))
218 }
219
220 /// Check if a field starts with a prefix
221 ///
222 /// # Examples
223 /// ```
224 /// .filter(|f| f.starts_with("name", "John"))
225 /// ```
226 pub fn starts_with(&self, field: &str, prefix: &str) -> bool {
227 self.doc.data.get(field).map_or(false, |v| match v {
228 Value::String(s) => s.starts_with(prefix),
229 _ => false,
230 })
231 }
232
233 /// Check if a field ends with a suffix
234 ///
235 /// # Examples
236 /// ```
237 /// .filter(|f| f.ends_with("name", "son"))
238 /// ```
239 pub fn ends_with(&self, field: &str, suffix: &str) -> bool {
240 self.doc.data.get(field).map_or(false, |v| match v {
241 Value::String(s) => s.ends_with(suffix),
242 _ => false,
243 })
244 }
245
246 /// Check if a field is in an array
247 ///
248 /// # Examples
249 /// ```
250 /// .filter(|f| f.array_contains("status", "active"))
251 /// ```
252 pub fn array_contains(&self, field: &str, value: impl Into<Value>) -> bool {
253 let value = value.into();
254 self.doc.data.get(field).map_or(false, |v| match v {
255 Value::Array(arr) => arr.contains(&value),
256 _ => false,
257 })
258 }
259
260 /// Check if an array has a specific length
261 ///
262 /// # Examples
263 /// ```
264 /// .filter(|f| f.array_len_eq("status", 2))
265 /// ```
266 pub fn array_len_eq(&self, field: &str, len: usize) -> bool {
267 self.doc.data.get(field).map_or(false, |v| match v {
268 Value::Array(arr) => arr.len() == len,
269 _ => false,
270 })
271 }
272
273 /// Access a nested field using dot notation
274 ///
275 /// # Examples
276 /// ```
277 /// .filter(|f| f.get_nested_value("user.address.city") == Some(&Value::String("New York")))
278 /// ```
279 pub fn get_nested_value(&self, path: &str) -> Option<&Value> {
280 let parts: Vec<&str> = path.split('.').collect();
281 let mut current = self.doc.data.get(parts[0])?;
282
283 for &part in &parts[1..] {
284 if let Value::Object(map) = current {
285 current = map.get(part)?;
286 } else {
287 return None;
288 }
289 }
290
291 Some(current)
292 }
293
294 /// Check if a nested field equals a value
295 ///
296 /// # Examples
297 /// ```
298 /// .filter(|f| f.nested_eq("user.address.city", "New York"))
299 /// ```
300 pub fn nested_eq<T: Into<Value>>(&self, path: &str, value: T) -> bool {
301 let value = value.into();
302 self.get_nested_value(path).map_or(false, |v| v == &value)
303 }
304
305 /// Check if a field matches a regular expression
306 ///
307 /// # Examples
308 /// ```
309 /// .filter(|f| f.matches_regex("email", r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"))
310 /// ```
311 pub fn matches_regex(&self, field: &str, pattern: &str) -> bool {
312 use regex::Regex;
313
314 if let Ok(re) = Regex::new(pattern) {
315 self.doc.data.get(field).map_or(false, |v| match v {
316 Value::String(s) => re.is_match(s),
317 _ => false,
318 })
319 } else {
320 false
321 }
322 }
323}
324
325impl<'a> QueryBuilder<'a> {
326 /// Create a new query builder for the specified collection
327 ///
328 /// # Examples
329 /// ```
330 /// let query = db.query("users");
331 /// ```
332 pub fn new(db: &'a Aurora, collection: &str) -> Self {
333 Self {
334 db,
335 collection: collection.to_string(),
336 filters: Vec::new(),
337 order_by: None,
338 limit: None,
339 offset: None,
340 fields: None,
341 }
342 }
343
344 /// Add a filter function to the query
345 ///
346 /// # Examples
347 /// ```
348 /// let active_users = db.query("users")
349 /// .filter(|f| f.eq("status", "active"))
350 /// .collect()
351 /// .await?;
352 /// ```
353 pub fn filter<F>(mut self, filter_fn: F) -> Self
354 where
355 // CHANGE 2: Require the closure `F` to be `Send`.
356 F: Fn(&FilterBuilder) -> bool + Send + 'a,
357 {
358 self.filters.push(Box::new(move |doc| {
359 let filter_builder = FilterBuilder::new(doc);
360 filter_fn(&filter_builder)
361 }));
362 self
363 }
364
365 /// Sort results by a field (ascending or descending)
366 ///
367 /// # Parameters
368 /// * `field` - The field to sort by
369 /// * `ascending` - `true` for ascending order, `false` for descending
370 ///
371 /// # Examples
372 /// ```
373 /// // Sort by age ascending
374 /// .order_by("age", true)
375 ///
376 /// // Sort by creation date descending (newest first)
377 /// .order_by("created_at", false)
378 /// ```
379 pub fn order_by(mut self, field: &str, ascending: bool) -> Self {
380 self.order_by = Some((field.to_string(), ascending));
381 self
382 }
383
384 /// Limit the number of results returned
385 ///
386 /// # Examples
387 /// ```
388 /// // Get at most 10 results
389 /// .limit(10)
390 /// ```
391 pub fn limit(mut self, limit: usize) -> Self {
392 self.limit = Some(limit);
393 self
394 }
395
396 /// Skip a number of results (for pagination)
397 ///
398 /// # Examples
399 /// ```
400 /// // For pagination: skip the first 20 results and get the next 10
401 /// .offset(20).limit(10)
402 /// ```
403 pub fn offset(mut self, offset: usize) -> Self {
404 self.offset = Some(offset);
405 self
406 }
407
408 /// Select only specific fields to return
409 ///
410 /// # Examples
411 /// ```
412 /// // Only return name and email fields
413 /// .select(&["name", "email"])
414 /// ```
415 pub fn select(mut self, fields: &[&str]) -> Self {
416 self.fields = Some(fields.iter().map(|s| s.to_string()).collect());
417 self
418 }
419
420 /// Execute the query and collect the results
421 ///
422 /// # Returns
423 /// A vector of documents matching the query criteria
424 ///
425 /// # Examples
426 /// ```
427 /// let results = db.query("products")
428 /// .filter(|f| f.lt("price", 100))
429 /// .collect()
430 /// .await?;
431 /// ```
432 pub async fn collect(self) -> Result<Vec<Document>> {
433 // Ensure indices are initialized
434 self.db.ensure_indices_initialized().await?;
435
436 let mut docs = self.db.get_all_collection(&self.collection).await?;
437
438 // Apply filters
439 docs.retain(|doc| self.filters.iter().all(|f| f(doc)));
440
441 // Apply ordering
442 if let Some((field, ascending)) = self.order_by {
443 docs.sort_by(|a, b| match (a.data.get(&field), b.data.get(&field)) {
444 (Some(v1), Some(v2)) => {
445 let cmp = v1.cmp(v2);
446 if ascending { cmp } else { cmp.reverse() }
447 }
448 (None, Some(_)) => std::cmp::Ordering::Less,
449 (Some(_), None) => std::cmp::Ordering::Greater,
450 (None, None) => std::cmp::Ordering::Equal,
451 });
452 }
453
454 // Apply field selection if specified
455 if let Some(fields) = self.fields {
456 // Create new documents with only selected fields
457 docs = docs
458 .into_iter()
459 .map(|doc| {
460 let mut new_data = HashMap::new();
461 // Always include the ID
462 for field in &fields {
463 if let Some(value) = doc.data.get(field) {
464 new_data.insert(field.clone(), value.clone());
465 }
466 }
467 Document {
468 id: doc.id,
469 data: new_data,
470 }
471 })
472 .collect();
473 }
474
475 // Apply offset and limit safely
476 let start = self.offset.unwrap_or(0);
477 let end = self
478 .limit
479 .map(|l| start.saturating_add(l))
480 .unwrap_or(docs.len());
481
482 // Ensure we don't go out of bounds
483 let end = end.min(docs.len());
484 Ok(docs.get(start..end).unwrap_or(&[]).to_vec())
485 }
486
487 /// Get only the first matching document or None if no matches
488 ///
489 /// # Examples
490 /// ```
491 /// let user = db.query("users")
492 /// .filter(|f| f.eq("email", "jane@example.com"))
493 /// .first_one()
494 /// .await?;
495 /// ```
496 pub async fn first_one(self) -> Result<Option<Document>> {
497 self.limit(1).collect().await.map(|mut docs| docs.pop())
498 }
499
500 /// Count the number of documents matching the query
501 ///
502 /// # Examples
503 /// ```
504 /// let active_count = db.query("users")
505 /// .filter(|f| f.eq("status", "active"))
506 /// .count()
507 /// .await?;
508 /// ```
509 pub async fn count(self) -> Result<usize> {
510 self.collect().await.map(|docs| docs.len())
511 }
512
513 /// Update documents matching the query with new field values
514 ///
515 /// # Returns
516 /// The number of documents updated
517 ///
518 /// # Examples
519 /// ```
520 /// let updated = db.query("products")
521 /// .filter(|f| f.lt("stock", 5))
522 /// .update([
523 /// ("status", Value::String("low_stock".to_string())),
524 /// ("needs_reorder", Value::Bool(true))
525 /// ].into_iter().collect())
526 /// .await?;
527 /// ```
528 pub async fn update(self, updates: HashMap<&str, Value>) -> Result<usize> {
529 // Store a reference to the db and collection before consuming self
530 let db = self.db;
531 let collection = self.collection.clone();
532
533 let docs = self.collect().await?;
534 let mut updated_count = 0;
535
536 for doc in docs {
537 let mut updated_doc = doc.clone();
538 let mut changed = false;
539
540 for (field, value) in &updates {
541 updated_doc.data.insert(field.to_string(), value.clone());
542 changed = true;
543 }
544
545 if changed {
546 // Update document in the database
547 db.put(
548 format!("{}:{}", collection, updated_doc.id),
549 serde_json::to_vec(&updated_doc)?,
550 None,
551 )?;
552 updated_count += 1;
553 }
554 }
555
556 Ok(updated_count)
557 }
558}
559
560/// Builder for performing full-text search operations
561///
562/// # Examples
563/// ```
564/// let results = db.search("products")
565/// .field("description")
566/// .matching("wireless headphones")
567/// .fuzzy(true)
568/// .collect()
569/// .await?;
570/// ```
571pub struct SearchBuilder<'a> {
572 db: &'a Aurora,
573 collection: String,
574 field: Option<String>,
575 query: Option<String>,
576 fuzzy: bool,
577}
578
579impl<'a> SearchBuilder<'a> {
580 /// Create a new search builder for the specified collection
581 pub fn new(db: &'a Aurora, collection: &str) -> Self {
582 Self {
583 db,
584 collection: collection.to_string(),
585 field: None,
586 query: None,
587 fuzzy: false,
588 }
589 }
590
591 /// Specify the field to search in
592 ///
593 /// # Examples
594 /// ```
595 /// .field("description")
596 /// ```
597 pub fn field(mut self, field: &str) -> Self {
598 self.field = Some(field.to_string());
599 self
600 }
601
602 /// Specify the search query text
603 ///
604 /// # Examples
605 /// ```
606 /// .matching("wireless headphones")
607 /// ```
608 pub fn matching(mut self, query: &str) -> Self {
609 self.query = Some(query.to_string());
610 self
611 }
612
613 /// Enable or disable fuzzy matching (for typo tolerance)
614 ///
615 /// # Examples
616 /// ```
617 /// .fuzzy(true) // Enable fuzzy matching
618 /// ```
619 pub fn fuzzy(mut self, enable: bool) -> Self {
620 self.fuzzy = enable;
621 self
622 }
623
624 /// Execute the search and collect the results
625 ///
626 /// # Returns
627 /// A vector of documents matching the search criteria
628 ///
629 /// # Examples
630 /// ```
631 /// let results = db.search("articles")
632 /// .field("content")
633 /// .matching("quantum computing")
634 /// .collect()
635 /// .await?;
636 /// ```
637 pub async fn collect(self) -> Result<Vec<Document>> {
638 let field = self
639 .field
640 .ok_or_else(|| AuroraError::InvalidOperation("Search field not specified".into()))?;
641 let query = self
642 .query
643 .ok_or_else(|| AuroraError::InvalidOperation("Search query not specified".into()))?;
644
645 self.db.search_text(&self.collection, &field, &query).await
646 }
647}
648
649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
650pub struct SimpleQueryBuilder {
651 pub collection: String,
652 pub filters: Vec<Filter>,
653}
654
655impl SimpleQueryBuilder {
656 pub fn new(collection: String) -> Self {
657 Self {
658 collection,
659 filters: Vec::new(),
660 }
661 }
662
663 pub fn filter(mut self, filter: Filter) -> Self {
664 self.filters.push(filter);
665 self
666 }
667
668 pub fn eq(self, field: &str, value: Value) -> Self {
669 self.filter(Filter::Eq(field.to_string(), value))
670 }
671
672 pub fn gt(self, field: &str, value: Value) -> Self {
673 self.filter(Filter::Gt(field.to_string(), value))
674 }
675
676 pub fn gte(self, field: &str, value: Value) -> Self {
677 self.filter(Filter::Gte(field.to_string(), value))
678 }
679
680 pub fn lt(self, field: &str, value: Value) -> Self {
681 self.filter(Filter::Lt(field.to_string(), value))
682 }
683
684 pub fn lte(self, field: &str, value: Value) -> Self {
685 self.filter(Filter::Lte(field.to_string(), value))
686 }
687
688 pub fn contains(self, field: &str, value: &str) -> Self {
689 self.filter(Filter::Contains(field.to_string(), value.to_string()))
690 }
691
692 /// Convenience method for range queries
693 pub fn between(self, field: &str, min: Value, max: Value) -> Self {
694 self.filter(Filter::Gte(field.to_string(), min))
695 .filter(Filter::Lte(field.to_string(), max))
696 }
697}
698
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700pub enum Filter {
701 Eq(String, Value),
702 Gt(String, Value),
703 Gte(String, Value),
704 Lt(String, Value),
705 Lte(String, Value),
706 Contains(String, String),
707}