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/// Type alias for document filter functions
43type DocumentFilter<'a> = Box<dyn Fn(&Document) -> bool + Send + Sync + 'a>;
44
45/// Builder for creating and executing document queries.
46///
47/// QueryBuilder uses a fluent interface pattern to construct
48/// and execute queries against Aurora collections.
49///
50/// # Examples
51///
52/// ```
53/// // Query for active premium users
54/// let premium_users = db.query("users")
55/// .filter(|f| f.eq("status", "active") && f.eq("account_type", "premium"))
56/// .order_by("created_at", false)
57/// .limit(10)
58/// .collect()
59/// .await?;
60/// ```
61pub struct QueryBuilder<'a> {
62 db: &'a Aurora,
63 collection: String,
64 filters: Vec<DocumentFilter<'a>>,
65 order_by: Option<(String, bool)>,
66 limit: Option<usize>,
67 offset: Option<usize>,
68 fields: Option<Vec<String>>,
69}
70
71/// Builder for constructing document filter expressions.
72///
73/// This struct provides methods for comparing document fields
74/// with values to create filter conditions.
75///
76/// # Examples
77///
78/// ```
79/// // Combine multiple filter conditions
80/// db.query("products")
81/// .filter(|f| {
82/// f.gte("price", 10.0) &&
83/// f.lte("price", 50.0) &&
84/// f.contains("name", "widget")
85/// })
86/// .collect()
87/// .await?;
88/// ```
89pub struct FilterBuilder<'a, 'b> {
90 doc: &'b Document,
91 _marker: std::marker::PhantomData<&'a ()>,
92}
93
94impl<'a, 'b> FilterBuilder<'a, 'b> {
95 /// Create a new filter builder for the given document
96 pub fn new(doc: &'b Document) -> Self {
97 Self {
98 doc,
99 _marker: std::marker::PhantomData,
100 }
101 }
102
103 /// Check if a field equals a value
104 ///
105 /// # Examples
106 /// ```
107 /// .filter(|f| f.eq("status", "active"))
108 /// ```
109 pub fn eq<T: Into<Value>>(&self, field: &str, value: T) -> bool {
110 let value = value.into();
111 self.doc.data.get(field) == Some(&value)
112 }
113
114 /// Check if a field is greater than a value
115 ///
116 /// # Examples
117 /// ```
118 /// .filter(|f| f.gt("age", 21))
119 /// ```
120 pub fn gt<T: Into<Value>>(&self, field: &str, value: T) -> bool {
121 let value = value.into();
122 self.doc.data.get(field).is_some_and(|v| v > &value)
123 }
124
125 /// Check if a field is greater than or equal to a value
126 ///
127 /// # Examples
128 /// ```
129 /// .filter(|f| f.gte("age", 21))
130 /// ```
131 pub fn gte<T: Into<Value>>(&self, field: &str, value: T) -> bool {
132 let value = value.into();
133 self.doc.data.get(field).is_some_and(|v| v >= &value)
134 }
135
136 /// Check if a field is less than a value
137 ///
138 /// # Examples
139 /// ```
140 /// .filter(|f| f.lt("age", 65))
141 /// ```
142 pub fn lt<T: Into<Value>>(&self, field: &str, value: T) -> bool {
143 let value = value.into();
144 self.doc.data.get(field).is_some_and(|v| v < &value)
145 }
146
147 /// Check if a field is less than or equal to a value
148 ///
149 /// # Examples
150 /// ```
151 /// .filter(|f| f.lte("age", 65))
152 /// ```
153 pub fn lte<T: Into<Value>>(&self, field: &str, value: T) -> bool {
154 let value = value.into();
155 self.doc.data.get(field).is_some_and(|v| v <= &value)
156 }
157
158 /// Check if a field contains a value
159 ///
160 /// # Examples
161 /// ```
162 /// .filter(|f| f.contains("name", "widget"))
163 /// ```
164 pub fn contains(&self, field: &str, value: &str) -> bool {
165 self.doc.data.get(field).is_some_and(|v| match v {
166 Value::String(s) => s.contains(value),
167 Value::Array(arr) => arr.contains(&Value::String(value.to_string())),
168 _ => false,
169 })
170 }
171
172 /// Check if a field is in a list of values
173 ///
174 /// # Examples
175 /// ```
176 /// .filter(|f| f.in_values("status", &["active", "inactive"]))
177 /// ```
178 pub fn in_values<T: Into<Value> + Clone>(&self, field: &str, values: &[T]) -> bool {
179 let values: Vec<Value> = values.iter().map(|v| v.clone().into()).collect();
180 self.doc
181 .data
182 .get(field)
183 .is_some_and(|v| values.contains(v))
184 }
185
186 /// Check if a field is between two values (inclusive)
187 ///
188 /// # Examples
189 /// ```
190 /// .filter(|f| f.between("age", 18, 65))
191 /// ```
192 pub fn between<T: Into<Value> + Clone>(&self, field: &str, min: T, max: T) -> bool {
193 self.gte(field, min) && self.lte(field, max)
194 }
195
196 /// Check if a field exists and is not null
197 ///
198 /// # Examples
199 /// ```
200 /// .filter(|f| f.exists("email"))
201 /// ```
202 pub fn exists(&self, field: &str) -> bool {
203 self.doc
204 .data
205 .get(field)
206 .is_some_and(|v| !matches!(v, Value::Null))
207 }
208
209 /// Check if a field doesn't exist or is null
210 ///
211 /// # Examples
212 /// ```
213 /// .filter(|f| f.is_null("email"))
214 /// ```
215 pub fn is_null(&self, field: &str) -> bool {
216 self.doc
217 .data
218 .get(field)
219 .is_none_or(|v| matches!(v, Value::Null))
220 }
221
222 /// Check if a field starts with a prefix
223 ///
224 /// # Examples
225 /// ```
226 /// .filter(|f| f.starts_with("name", "John"))
227 /// ```
228 pub fn starts_with(&self, field: &str, prefix: &str) -> bool {
229 self.doc.data.get(field).is_some_and(|v| match v {
230 Value::String(s) => s.starts_with(prefix),
231 _ => false,
232 })
233 }
234
235 /// Check if a field ends with a suffix
236 ///
237 /// # Examples
238 /// ```
239 /// .filter(|f| f.ends_with("name", "son"))
240 /// ```
241 pub fn ends_with(&self, field: &str, suffix: &str) -> bool {
242 self.doc.data.get(field).is_some_and(|v| match v {
243 Value::String(s) => s.ends_with(suffix),
244 _ => false,
245 })
246 }
247
248 /// Check if a field is in an array
249 ///
250 /// # Examples
251 /// ```
252 /// .filter(|f| f.array_contains("status", "active"))
253 /// ```
254 pub fn array_contains(&self, field: &str, value: impl Into<Value>) -> bool {
255 let value = value.into();
256 self.doc.data.get(field).is_some_and(|v| match v {
257 Value::Array(arr) => arr.contains(&value),
258 _ => false,
259 })
260 }
261
262 /// Check if an array has a specific length
263 ///
264 /// # Examples
265 /// ```
266 /// .filter(|f| f.array_len_eq("status", 2))
267 /// ```
268 pub fn array_len_eq(&self, field: &str, len: usize) -> bool {
269 self.doc.data.get(field).is_some_and(|v| match v {
270 Value::Array(arr) => arr.len() == len,
271 _ => false,
272 })
273 }
274
275 /// Access a nested field using dot notation
276 ///
277 /// # Examples
278 /// ```
279 /// .filter(|f| f.get_nested_value("user.address.city") == Some(&Value::String("New York")))
280 /// ```
281 pub fn get_nested_value(&self, path: &str) -> Option<&Value> {
282 let parts: Vec<&str> = path.split('.').collect();
283 let mut current = self.doc.data.get(parts[0])?;
284
285 for &part in &parts[1..] {
286 if let Value::Object(map) = current {
287 current = map.get(part)?;
288 } else {
289 return None;
290 }
291 }
292
293 Some(current)
294 }
295
296 /// Check if a nested field equals a value
297 ///
298 /// # Examples
299 /// ```
300 /// .filter(|f| f.nested_eq("user.address.city", "New York"))
301 /// ```
302 pub fn nested_eq<T: Into<Value>>(&self, path: &str, value: T) -> bool {
303 let value = value.into();
304 self.get_nested_value(path) == Some(&value)
305 }
306
307 /// Check if a field matches a regular expression
308 ///
309 /// # Examples
310 /// ```
311 /// .filter(|f| f.matches_regex("email", r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"))
312 /// ```
313 pub fn matches_regex(&self, field: &str, pattern: &str) -> bool {
314 use regex::Regex;
315
316 if let Ok(re) = Regex::new(pattern) {
317 self.doc.data.get(field).is_some_and(|v| match v {
318 Value::String(s) => re.is_match(s),
319 _ => false,
320 })
321 } else {
322 false
323 }
324 }
325}
326
327impl<'a> QueryBuilder<'a> {
328 /// Create a new query builder for the specified collection
329 ///
330 /// # Examples
331 /// ```
332 /// let query = db.query("users");
333 /// ```
334 pub fn new(db: &'a Aurora, collection: &str) -> Self {
335 Self {
336 db,
337 collection: collection.to_string(),
338 filters: Vec::new(),
339 order_by: None,
340 limit: None,
341 offset: None,
342 fields: None,
343 }
344 }
345
346 /// Add a filter function to the query
347 ///
348 /// # Examples
349 /// ```
350 /// let active_users = db.query("users")
351 /// .filter(|f| f.eq("status", "active"))
352 /// .collect()
353 /// .await?;
354 /// ```
355 pub fn filter<F>(mut self, filter_fn: F) -> Self
356 where
357 // CHANGE 2: Require the closure `F` to be `Send + Sync`.
358 F: Fn(&FilterBuilder) -> bool + Send + Sync + 'a,
359 {
360 self.filters.push(Box::new(move |doc| {
361 let filter_builder = FilterBuilder::new(doc);
362 filter_fn(&filter_builder)
363 }));
364 self
365 }
366
367 /// Sort results by a field (ascending or descending)
368 ///
369 /// # Parameters
370 /// * `field` - The field to sort by
371 /// * `ascending` - `true` for ascending order, `false` for descending
372 ///
373 /// # Examples
374 /// ```
375 /// // Sort by age ascending
376 /// .order_by("age", true)
377 ///
378 /// // Sort by creation date descending (newest first)
379 /// .order_by("created_at", false)
380 /// ```
381 pub fn order_by(mut self, field: &str, ascending: bool) -> Self {
382 self.order_by = Some((field.to_string(), ascending));
383 self
384 }
385
386 /// Limit the number of results returned
387 ///
388 /// # Examples
389 /// ```
390 /// // Get at most 10 results
391 /// .limit(10)
392 /// ```
393 pub fn limit(mut self, limit: usize) -> Self {
394 self.limit = Some(limit);
395 self
396 }
397
398 /// Skip a number of results (for pagination)
399 ///
400 /// # Examples
401 /// ```
402 /// // For pagination: skip the first 20 results and get the next 10
403 /// .offset(20).limit(10)
404 /// ```
405 pub fn offset(mut self, offset: usize) -> Self {
406 self.offset = Some(offset);
407 self
408 }
409
410 /// Select only specific fields to return
411 ///
412 /// # Examples
413 /// ```
414 /// // Only return name and email fields
415 /// .select(&["name", "email"])
416 /// ```
417 pub fn select(mut self, fields: &[&str]) -> Self {
418 self.fields = Some(fields.iter().map(|s| s.to_string()).collect());
419 self
420 }
421
422 /// Execute the query and collect the results
423 ///
424 /// # Returns
425 /// A vector of documents matching the query criteria
426 ///
427 /// # Examples
428 /// ```
429 /// let results = db.query("products")
430 /// .filter(|f| f.lt("price", 100))
431 /// .collect()
432 /// .await?;
433 /// ```
434 pub async fn collect(self) -> Result<Vec<Document>> {
435 // Ensure indices are initialized
436 self.db.ensure_indices_initialized().await?;
437
438 // Optimization: Use early termination for queries with LIMIT but no ORDER BY
439 let mut docs = if self.order_by.is_none() && self.limit.is_some() {
440 // Early termination path - scan only until we have enough results
441 let target = self.limit.unwrap() + self.offset.unwrap_or(0);
442 let filter = |doc: &Document| self.filters.iter().all(|f| f(doc));
443 self.db.scan_and_filter(&self.collection, filter, Some(target))?
444 } else {
445 // Standard path - need all matching docs (for sorting or no limit)
446 let mut docs = self.db.get_all_collection(&self.collection).await?;
447 docs.retain(|doc| self.filters.iter().all(|f| f(doc)));
448 docs
449 };
450
451 // Apply ordering
452 if let Some((field, ascending)) = self.order_by {
453 docs.sort_by(|a, b| match (a.data.get(&field), b.data.get(&field)) {
454 (Some(v1), Some(v2)) => {
455 let cmp = v1.cmp(v2);
456 if ascending { cmp } else { cmp.reverse() }
457 }
458 (None, Some(_)) => std::cmp::Ordering::Less,
459 (Some(_), None) => std::cmp::Ordering::Greater,
460 (None, None) => std::cmp::Ordering::Equal,
461 });
462 }
463
464 // Apply field selection if specified
465 if let Some(fields) = self.fields {
466 // Create new documents with only selected fields
467 docs = docs
468 .into_iter()
469 .map(|doc| {
470 let mut new_data = HashMap::new();
471 // Always include the ID
472 for field in &fields {
473 if let Some(value) = doc.data.get(field) {
474 new_data.insert(field.clone(), value.clone());
475 }
476 }
477 Document {
478 id: doc.id,
479 data: new_data,
480 }
481 })
482 .collect();
483 }
484
485 // Apply offset and limit safely
486 let start = self.offset.unwrap_or(0);
487 let end = self
488 .limit
489 .map(|l| start.saturating_add(l))
490 .unwrap_or(docs.len());
491
492 // Ensure we don't go out of bounds
493 let end = end.min(docs.len());
494 Ok(docs.get(start..end).unwrap_or(&[]).to_vec())
495 }
496
497 /// Watch the query for real-time updates
498 ///
499 /// Returns a QueryWatcher that streams live updates when documents are added,
500 /// removed, or modified in ways that affect the query results. Perfect for
501 /// building reactive UIs, live dashboards, and real-time applications.
502 ///
503 /// # Performance
504 /// - Zero overhead for queries without watchers
505 /// - Updates delivered asynchronously via channels
506 /// - Automatic filtering - only matching changes are emitted
507 /// - Memory efficient - only tracks matching documents
508 ///
509 /// # Requirements
510 /// This method requires the QueryBuilder to have a 'static lifetime,
511 /// which means the database reference must also be 'static (e.g., Arc<Aurora>).
512 ///
513 /// # Examples
514 ///
515 /// ```
516 /// use aurora_db::{Aurora, types::Value};
517 /// use std::sync::Arc;
518 ///
519 /// let db = Arc::new(Aurora::open("mydb.db")?);
520 ///
521 /// // Basic reactive query - watch active users
522 /// let mut watcher = db.query("users")
523 /// .filter(|f| f.eq("active", Value::Bool(true)))
524 /// .watch()
525 /// .await?;
526 ///
527 /// // Receive updates in real-time
528 /// while let Some(update) = watcher.next().await {
529 /// match update {
530 /// QueryUpdate::Added(doc) => {
531 /// println!("New active user: {}", doc.id);
532 /// },
533 /// QueryUpdate::Removed(doc) => {
534 /// println!("User deactivated: {}", doc.id);
535 /// },
536 /// QueryUpdate::Modified { old, new } => {
537 /// println!("User updated: {} -> {}", old.id, new.id);
538 /// },
539 /// }
540 /// }
541 /// ```
542 ///
543 /// # Real-World Use Cases
544 ///
545 /// **Live Leaderboard:**
546 /// ```
547 /// // Watch top players by score
548 /// let mut leaderboard = db.query("players")
549 /// .filter(|f| f.gte("score", Value::Int(1000)))
550 /// .watch()
551 /// .await?;
552 ///
553 /// tokio::spawn(async move {
554 /// while let Some(update) = leaderboard.next().await {
555 /// // Update UI with new rankings
556 /// broadcast_to_clients(&update).await;
557 /// }
558 /// });
559 /// ```
560 ///
561 /// **Activity Feed:**
562 /// ```
563 /// // Watch recent posts for a user's feed
564 /// let mut feed = db.query("posts")
565 /// .filter(|f| f.eq("author_id", user_id))
566 /// .watch()
567 /// .await?;
568 ///
569 /// // Stream updates to WebSocket
570 /// while let Some(update) = feed.next().await {
571 /// match update {
572 /// QueryUpdate::Added(post) => {
573 /// websocket.send(json!({"type": "new_post", "post": post})).await?;
574 /// },
575 /// _ => {}
576 /// }
577 /// }
578 /// ```
579 ///
580 /// **Real-Time Dashboard:**
581 /// ```
582 /// // Watch critical metrics
583 /// let mut alerts = db.query("metrics")
584 /// .filter(|f| f.gt("cpu_usage", Value::Float(80.0)))
585 /// .watch()
586 /// .await?;
587 ///
588 /// tokio::spawn(async move {
589 /// while let Some(update) = alerts.next().await {
590 /// if let QueryUpdate::Added(metric) = update {
591 /// // Alert on high CPU usage
592 /// send_alert(format!("High CPU: {:?}", metric)).await;
593 /// }
594 /// }
595 /// });
596 /// ```
597 ///
598 /// **Collaborative Editing:**
599 /// ```
600 /// // Watch document for changes from other users
601 /// let doc_id = "doc-123";
602 /// let mut changes = db.query("documents")
603 /// .filter(|f| f.eq("id", doc_id))
604 /// .watch()
605 /// .await?;
606 ///
607 /// tokio::spawn(async move {
608 /// while let Some(update) = changes.next().await {
609 /// if let QueryUpdate::Modified { old, new } = update {
610 /// // Merge changes from other editors
611 /// apply_remote_changes(&old, &new).await;
612 /// }
613 /// }
614 /// });
615 /// ```
616 ///
617 /// **Stock Ticker:**
618 /// ```
619 /// // Watch price changes
620 /// let mut price_watcher = db.query("stocks")
621 /// .filter(|f| f.eq("symbol", "AAPL"))
622 /// .watch()
623 /// .await?;
624 ///
625 /// while let Some(update) = price_watcher.next().await {
626 /// if let QueryUpdate::Modified { old, new } = update {
627 /// if let (Some(old_price), Some(new_price)) =
628 /// (old.data.get("price"), new.data.get("price")) {
629 /// println!("AAPL: {} -> {}", old_price, new_price);
630 /// }
631 /// }
632 /// }
633 /// ```
634 ///
635 /// # Multiple Watchers Pattern
636 ///
637 /// ```
638 /// // Watch multiple queries concurrently
639 /// let mut high_priority = db.query("tasks")
640 /// .filter(|f| f.eq("priority", Value::String("high".into())))
641 /// .watch()
642 /// .await?;
643 ///
644 /// let mut urgent = db.query("tasks")
645 /// .filter(|f| f.eq("status", Value::String("urgent".into())))
646 /// .watch()
647 /// .await?;
648 ///
649 /// tokio::spawn(async move {
650 /// loop {
651 /// tokio::select! {
652 /// Some(update) = high_priority.next() => {
653 /// println!("High priority: {:?}", update);
654 /// },
655 /// Some(update) = urgent.next() => {
656 /// println!("Urgent: {:?}", update);
657 /// },
658 /// }
659 /// }
660 /// });
661 /// ```
662 ///
663 /// # Important Notes
664 /// - Requires Arc<Aurora> for 'static lifetime
665 /// - Updates are delivered asynchronously
666 /// - Watcher keeps running until dropped
667 /// - Only matching documents trigger updates
668 /// - Use tokio::spawn to process updates in background
669 ///
670 /// # See Also
671 /// - `Aurora::listen()` for collection-level change notifications
672 /// - `QueryWatcher::next()` to receive the next update
673 /// - `QueryWatcher::try_next()` for non-blocking checks
674 pub async fn watch(mut self) -> Result<crate::reactive::QueryWatcher>
675 where
676 'a: 'static,
677 {
678 use crate::reactive::{QueryWatcher, ReactiveQueryState};
679 use std::sync::Arc;
680
681 // Extract the filters before consuming self
682 let collection = self.collection.clone();
683 let db = self.db;
684 let filters = std::mem::take(&mut self.filters);
685
686 // Get initial results
687 let docs = self.collect().await?;
688
689 // Create a listener for this collection
690 let listener = db.listen(&collection);
691
692 // Create filter closure that combines all the query filters
693 let filter_fn = move |doc: &Document| -> bool { filters.iter().all(|f| f(doc)) };
694
695 // Create reactive state
696 let state = Arc::new(ReactiveQueryState::new(filter_fn));
697
698 // Create and return watcher
699 Ok(QueryWatcher::new(collection, listener, state, docs))
700 }
701
702 /// Get only the first matching document or None if no matches
703 ///
704 /// # Examples
705 /// ```
706 /// let user = db.query("users")
707 /// .filter(|f| f.eq("email", "jane@example.com"))
708 /// .first_one()
709 /// .await?;
710 /// ```
711 pub async fn first_one(self) -> Result<Option<Document>> {
712 self.limit(1).collect().await.map(|mut docs| docs.pop())
713 }
714
715 /// Count the number of documents matching the query
716 ///
717 /// # Examples
718 /// ```
719 /// let active_count = db.query("users")
720 /// .filter(|f| f.eq("status", "active"))
721 /// .count()
722 /// .await?;
723 /// ```
724 pub async fn count(self) -> Result<usize> {
725 self.collect().await.map(|docs| docs.len())
726 }
727
728 /// Update documents matching the query with new field values
729 ///
730 /// # Returns
731 /// The number of documents updated
732 ///
733 /// # Examples
734 /// ```
735 /// let updated = db.query("products")
736 /// .filter(|f| f.lt("stock", 5))
737 /// .update([
738 /// ("status", Value::String("low_stock".to_string())),
739 /// ("needs_reorder", Value::Bool(true))
740 /// ].into_iter().collect())
741 /// .await?;
742 /// ```
743 pub async fn update(self, updates: HashMap<&str, Value>) -> Result<usize> {
744 // Store a reference to the db and collection before consuming self
745 let db = self.db;
746 let collection = self.collection.clone();
747
748 let docs = self.collect().await?;
749 let mut updated_count = 0;
750
751 for doc in docs {
752 let mut updated_doc = doc.clone();
753 let mut changed = false;
754
755 for (field, value) in &updates {
756 updated_doc.data.insert(field.to_string(), value.clone());
757 changed = true;
758 }
759
760 if changed {
761 // Update document in the database
762 db.put(
763 format!("{}:{}", collection, updated_doc.id),
764 serde_json::to_vec(&updated_doc)?,
765 None,
766 )?;
767 updated_count += 1;
768 }
769 }
770
771 Ok(updated_count)
772 }
773}
774
775/// Builder for performing full-text search operations
776///
777/// # Examples
778/// ```
779/// let results = db.search("products")
780/// .field("description")
781/// .matching("wireless headphones")
782/// .fuzzy(true)
783/// .collect()
784/// .await?;
785/// ```
786pub struct SearchBuilder<'a> {
787 db: &'a Aurora,
788 collection: String,
789 field: Option<String>,
790 query: Option<String>,
791 fuzzy: bool,
792}
793
794impl<'a> SearchBuilder<'a> {
795 /// Create a new search builder for the specified collection
796 pub fn new(db: &'a Aurora, collection: &str) -> Self {
797 Self {
798 db,
799 collection: collection.to_string(),
800 field: None,
801 query: None,
802 fuzzy: false,
803 }
804 }
805
806 /// Specify the field to search in
807 ///
808 /// # Examples
809 /// ```
810 /// .field("description")
811 /// ```
812 pub fn field(mut self, field: &str) -> Self {
813 self.field = Some(field.to_string());
814 self
815 }
816
817 /// Specify the search query text
818 ///
819 /// # Examples
820 /// ```
821 /// .matching("wireless headphones")
822 /// ```
823 pub fn matching(mut self, query: &str) -> Self {
824 self.query = Some(query.to_string());
825 self
826 }
827
828 /// Enable or disable fuzzy matching (for typo tolerance)
829 ///
830 /// # Examples
831 /// ```
832 /// .fuzzy(true) // Enable fuzzy matching
833 /// ```
834 pub fn fuzzy(mut self, enable: bool) -> Self {
835 self.fuzzy = enable;
836 self
837 }
838
839 /// Execute the search and collect the results
840 ///
841 /// # Returns
842 /// A vector of documents matching the search criteria
843 ///
844 /// # Examples
845 /// ```
846 /// let results = db.search("articles")
847 /// .field("content")
848 /// .matching("quantum computing")
849 /// .collect()
850 /// .await?;
851 /// ```
852 pub async fn collect(self) -> Result<Vec<Document>> {
853 let field = self
854 .field
855 .ok_or_else(|| AuroraError::InvalidOperation("Search field not specified".into()))?;
856 let query = self
857 .query
858 .ok_or_else(|| AuroraError::InvalidOperation("Search query not specified".into()))?;
859
860 self.db.search_text(&self.collection, &field, &query).await
861 }
862}
863
864#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
865pub struct SimpleQueryBuilder {
866 pub collection: String,
867 pub filters: Vec<Filter>,
868 pub order_by: Option<(String, bool)>,
869 pub limit: Option<usize>,
870 pub offset: Option<usize>,
871}
872
873impl SimpleQueryBuilder {
874 pub fn new(collection: String) -> Self {
875 Self {
876 collection,
877 filters: Vec::new(),
878 order_by: None,
879 limit: None,
880 offset: None,
881 }
882 }
883
884 pub fn filter(mut self, filter: Filter) -> Self {
885 self.filters.push(filter);
886 self
887 }
888
889 /// Filter for exact equality
890 ///
891 /// Uses secondary index if the field is indexed (O(1) lookup).
892 /// Falls back to full collection scan if not indexed (O(n)).
893 ///
894 /// # Arguments
895 /// * `field` - The field name to filter on
896 /// * `value` - The exact value to match
897 ///
898 /// # Examples
899 ///
900 /// ```
901 /// use aurora_db::{Aurora, types::Value};
902 ///
903 /// let db = Aurora::open("mydb.db")?;
904 ///
905 /// // Find active users
906 /// let active_users = db.query("users")
907 /// .filter(|f| f.eq("status", Value::String("active".into())))
908 /// .collect()
909 /// .await?;
910 ///
911 /// // Multiple equality filters (AND logic)
912 /// let premium_active = db.query("users")
913 /// .filter(|f| f.eq("tier", Value::String("premium".into())))
914 /// .filter(|f| f.eq("active", Value::Bool(true)))
915 /// .collect()
916 /// .await?;
917 ///
918 /// // Numeric equality
919 /// let age_30 = db.query("users")
920 /// .filter(|f| f.eq("age", Value::Int(30)))
921 /// .collect()
922 /// .await?;
923 /// ```
924 pub fn eq(self, field: &str, value: Value) -> Self {
925 self.filter(Filter::Eq(field.to_string(), value))
926 }
927
928 /// Filter for greater than
929 ///
930 /// Finds all documents where the field value is strictly greater than
931 /// the provided value. With LIMIT, uses early termination for performance.
932 ///
933 /// # Arguments
934 /// * `field` - The field name to compare
935 /// * `value` - The minimum value (exclusive)
936 ///
937 /// # Performance
938 /// - Without LIMIT: O(n) - scans all documents
939 /// - With LIMIT: O(k) where k = limit + offset (early termination)
940 /// - No index support yet (planned for future)
941 ///
942 /// # Examples
943 ///
944 /// ```
945 /// use aurora_db::{Aurora, types::Value};
946 ///
947 /// let db = Aurora::open("mydb.db")?;
948 ///
949 /// // Find high scorers (with early termination)
950 /// let high_scorers = db.query("users")
951 /// .filter(|f| f.gt("score", Value::Int(1000)))
952 /// .limit(100) // Stops after finding 100 matches
953 /// .collect()
954 /// .await?;
955 ///
956 /// // Price range queries
957 /// let expensive = db.query("products")
958 /// .filter(|f| f.gt("price", Value::Float(99.99)))
959 /// .order_by("price", false) // Descending
960 /// .collect()
961 /// .await?;
962 ///
963 /// // Date filtering (timestamps as integers)
964 /// let recent = db.query("events")
965 /// .filter(|f| f.gt("timestamp", Value::Int(1609459200))) // After Jan 1, 2021
966 /// .collect()
967 /// .await?;
968 /// ```
969 pub fn gt(self, field: &str, value: Value) -> Self {
970 self.filter(Filter::Gt(field.to_string(), value))
971 }
972
973 /// Filter for greater than or equal to
974 ///
975 /// Finds all documents where the field value is greater than or equal to
976 /// the provided value. Inclusive version of `gt()`.
977 ///
978 /// # Arguments
979 /// * `field` - The field name to compare
980 /// * `value` - The minimum value (inclusive)
981 ///
982 /// # Examples
983 ///
984 /// ```
985 /// use aurora_db::{Aurora, types::Value};
986 ///
987 /// let db = Aurora::open("mydb.db")?;
988 ///
989 /// // Minimum age requirement (inclusive)
990 /// let adults = db.query("users")
991 /// .filter(|f| f.gte("age", Value::Int(18)))
992 /// .collect()
993 /// .await?;
994 ///
995 /// // Inventory management
996 /// let in_stock = db.query("products")
997 /// .filter(|f| f.gte("stock", Value::Int(1)))
998 /// .collect()
999 /// .await?;
1000 /// ```
1001 pub fn gte(self, field: &str, value: Value) -> Self {
1002 self.filter(Filter::Gte(field.to_string(), value))
1003 }
1004
1005 /// Filter for less than
1006 ///
1007 /// Finds all documents where the field value is strictly less than
1008 /// the provided value.
1009 ///
1010 /// # Arguments
1011 /// * `field` - The field name to compare
1012 /// * `value` - The maximum value (exclusive)
1013 ///
1014 /// # Examples
1015 ///
1016 /// ```
1017 /// use aurora_db::{Aurora, types::Value};
1018 ///
1019 /// let db = Aurora::open("mydb.db")?;
1020 ///
1021 /// // Low balance accounts
1022 /// let low_balance = db.query("accounts")
1023 /// .filter(|f| f.lt("balance", Value::Float(10.0)))
1024 /// .collect()
1025 /// .await?;
1026 ///
1027 /// // Budget products
1028 /// let budget = db.query("products")
1029 /// .filter(|f| f.lt("price", Value::Float(50.0)))
1030 /// .order_by("price", true) // Ascending
1031 /// .collect()
1032 /// .await?;
1033 /// ```
1034 pub fn lt(self, field: &str, value: Value) -> Self {
1035 self.filter(Filter::Lt(field.to_string(), value))
1036 }
1037
1038 /// Filter for less than or equal to
1039 ///
1040 /// Finds all documents where the field value is less than or equal to
1041 /// the provided value. Inclusive version of `lt()`.
1042 ///
1043 /// # Arguments
1044 /// * `field` - The field name to compare
1045 /// * `value` - The maximum value (inclusive)
1046 ///
1047 /// # Examples
1048 ///
1049 /// ```
1050 /// use aurora_db::{Aurora, types::Value};
1051 ///
1052 /// let db = Aurora::open("mydb.db")?;
1053 ///
1054 /// // Senior discount eligibility
1055 /// let seniors = db.query("users")
1056 /// .filter(|f| f.lte("age", Value::Int(65)))
1057 /// .collect()
1058 /// .await?;
1059 ///
1060 /// // Clearance items
1061 /// let clearance = db.query("products")
1062 /// .filter(|f| f.lte("price", Value::Float(20.0)))
1063 /// .collect()
1064 /// .await?;
1065 /// ```
1066 pub fn lte(self, field: &str, value: Value) -> Self {
1067 self.filter(Filter::Lte(field.to_string(), value))
1068 }
1069
1070 /// Filter for substring containment
1071 ///
1072 /// Finds all documents where the field value contains the specified substring.
1073 /// Case-sensitive matching. For text search, consider using the `search()` API instead.
1074 ///
1075 /// # Arguments
1076 /// * `field` - The field name to search in (must be a string field)
1077 /// * `value` - The substring to search for
1078 ///
1079 /// # Performance
1080 /// - Always O(n) - scans all documents
1081 /// - Case-sensitive string matching
1082 /// - For full-text search, use `db.search()` instead
1083 ///
1084 /// # Examples
1085 ///
1086 /// ```
1087 /// use aurora_db::Aurora;
1088 ///
1089 /// let db = Aurora::open("mydb.db")?;
1090 ///
1091 /// // Find articles about Rust
1092 /// let rust_articles = db.query("articles")
1093 /// .filter(|f| f.contains("title", "Rust"))
1094 /// .collect()
1095 /// .await?;
1096 ///
1097 /// // Email domain filtering
1098 /// let gmail_users = db.query("users")
1099 /// .filter(|f| f.contains("email", "@gmail.com"))
1100 /// .collect()
1101 /// .await?;
1102 ///
1103 /// // Tag searching
1104 /// let rust_posts = db.query("posts")
1105 /// .filter(|f| f.contains("tags", "rust"))
1106 /// .collect()
1107 /// .await?;
1108 /// ```
1109 ///
1110 /// # Note
1111 /// For case-insensitive search or more advanced text matching,
1112 /// use the full-text search API: `db.search(collection).query(text)`
1113 pub fn contains(self, field: &str, value: &str) -> Self {
1114 self.filter(Filter::Contains(field.to_string(), value.to_string()))
1115 }
1116
1117 /// Convenience method for range queries
1118 pub fn between(self, field: &str, min: Value, max: Value) -> Self {
1119 self.filter(Filter::Gte(field.to_string(), min))
1120 .filter(Filter::Lte(field.to_string(), max))
1121 }
1122
1123 /// Sort results by a field (ascending or descending)
1124 pub fn order_by(mut self, field: &str, ascending: bool) -> Self {
1125 self.order_by = Some((field.to_string(), ascending));
1126 self
1127 }
1128
1129 /// Limit the number of results returned
1130 pub fn limit(mut self, limit: usize) -> Self {
1131 self.limit = Some(limit);
1132 self
1133 }
1134
1135 /// Skip a number of results (for pagination)
1136 pub fn offset(mut self, offset: usize) -> Self {
1137 self.offset = Some(offset);
1138 self
1139 }
1140}
1141
1142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1143pub enum Filter {
1144 Eq(String, Value),
1145 Gt(String, Value),
1146 Gte(String, Value),
1147 Lt(String, Value),
1148 Lte(String, Value),
1149 Contains(String, String),
1150}