prax_query/
pool.rs

1//! Arena-based filter pool for efficient nested filter construction.
2//!
3//! This module provides a `FilterPool` that uses bump allocation to efficiently
4//! construct complex nested filter trees with minimal allocations.
5//!
6//! # When to Use
7//!
8//! Use `FilterPool` when:
9//! - Building deeply nested filter trees (depth > 3)
10//! - Constructing many filters in a tight loop
11//! - Performance profiling shows filter allocation as a bottleneck
12//!
13//! For simple filters, use the regular `Filter` constructors directly.
14//!
15//! # Examples
16//!
17//! ## Basic Usage
18//!
19//! ```rust
20//! use prax_query::pool::FilterPool;
21//! use prax_query::{Filter, FilterValue};
22//!
23//! let mut pool = FilterPool::new();
24//!
25//! // Build a complex nested filter efficiently
26//! let filter = pool.build(|b| {
27//!     b.and(vec![
28//!         b.eq("status", "active"),
29//!         b.or(vec![
30//!             b.gt("age", 18),
31//!             b.eq("verified", true),
32//!         ]),
33//!         b.not(b.eq("deleted", true)),
34//!     ])
35//! });
36//!
37//! // The filter is now a regular owned Filter
38//! assert!(!filter.is_none());
39//! ```
40//!
41//! ## Reusing the Pool
42//!
43//! ```rust
44//! use prax_query::pool::FilterPool;
45//!
46//! let mut pool = FilterPool::new();
47//!
48//! // Build first filter
49//! let filter1 = pool.build(|b| b.eq("id", 1));
50//!
51//! // Reset and reuse the pool
52//! pool.reset();
53//!
54//! // Build second filter (reuses the same memory)
55//! let filter2 = pool.build(|b| b.eq("id", 2));
56//! ```
57
58use bumpalo::Bump;
59use std::borrow::Cow;
60
61use crate::filter::{Filter, FilterValue, FieldName, ValueList};
62
63/// A memory pool for efficient filter construction.
64///
65/// Uses bump allocation to minimize allocations when building complex filter trees.
66/// The pool can be reused by calling `reset()` after each filter is built.
67///
68/// # Performance
69///
70/// - Filter construction in the pool: O(1) allocation per filter tree
71/// - Materialization to owned filter: O(n) where n is the number of nodes
72/// - Pool reset: O(1) (just resets the bump pointer)
73///
74/// # Thread Safety
75///
76/// `FilterPool` is not thread-safe. Each thread should have its own pool.
77pub struct FilterPool {
78    arena: Bump,
79}
80
81impl FilterPool {
82    /// Create a new filter pool with default capacity.
83    ///
84    /// The pool starts with a small initial allocation and grows as needed.
85    pub fn new() -> Self {
86        Self {
87            arena: Bump::new(),
88        }
89    }
90
91    /// Create a new filter pool with the specified initial capacity in bytes.
92    ///
93    /// Use this when you know approximately how much memory your filters will need.
94    pub fn with_capacity(capacity: usize) -> Self {
95        Self {
96            arena: Bump::with_capacity(capacity),
97        }
98    }
99
100    /// Reset the pool, freeing all allocated memory for reuse.
101    ///
102    /// This is very fast (O(1)) as it just resets the bump pointer.
103    /// Call this between filter constructions to reuse memory.
104    pub fn reset(&mut self) {
105        self.arena.reset();
106    }
107
108    /// Get the amount of memory currently allocated in the pool.
109    pub fn allocated_bytes(&self) -> usize {
110        self.arena.allocated_bytes()
111    }
112
113    /// Build a filter using the pool's arena for temporary allocations.
114    ///
115    /// The closure receives a `FilterBuilder` that provides efficient methods
116    /// for constructing nested filters. The resulting filter is materialized
117    /// into an owned `Filter` that can be used after the pool is reset.
118    ///
119    /// # Examples
120    ///
121    /// ```rust
122    /// use prax_query::pool::FilterPool;
123    /// use prax_query::Filter;
124    ///
125    /// let mut pool = FilterPool::new();
126    /// let filter = pool.build(|b| {
127    ///     b.and(vec![
128    ///         b.eq("active", true),
129    ///         b.gt("score", 100),
130    ///     ])
131    /// });
132    /// ```
133    pub fn build<F>(&self, f: F) -> Filter
134    where
135        F: for<'a> FnOnce(&'a FilterBuilder<'a>) -> PooledFilter<'a>,
136    {
137        let builder = FilterBuilder::new(&self.arena);
138        let pooled = f(&builder);
139        pooled.materialize()
140    }
141}
142
143impl Default for FilterPool {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149/// A filter that lives in the pool's arena.
150///
151/// This is a temporary representation used during filter construction.
152/// Call `materialize()` to convert it to an owned `Filter`.
153#[derive(Debug, Clone, Copy)]
154pub enum PooledFilter<'a> {
155    /// No filter (always true).
156    None,
157    /// Equals comparison.
158    Equals(&'a str, PooledValue<'a>),
159    /// Not equals comparison.
160    NotEquals(&'a str, PooledValue<'a>),
161    /// Less than comparison.
162    Lt(&'a str, PooledValue<'a>),
163    /// Less than or equal comparison.
164    Lte(&'a str, PooledValue<'a>),
165    /// Greater than comparison.
166    Gt(&'a str, PooledValue<'a>),
167    /// Greater than or equal comparison.
168    Gte(&'a str, PooledValue<'a>),
169    /// In a list of values.
170    In(&'a str, &'a [PooledValue<'a>]),
171    /// Not in a list of values.
172    NotIn(&'a str, &'a [PooledValue<'a>]),
173    /// Contains (LIKE %value%).
174    Contains(&'a str, PooledValue<'a>),
175    /// Starts with (LIKE value%).
176    StartsWith(&'a str, PooledValue<'a>),
177    /// Ends with (LIKE %value).
178    EndsWith(&'a str, PooledValue<'a>),
179    /// Is null check.
180    IsNull(&'a str),
181    /// Is not null check.
182    IsNotNull(&'a str),
183    /// Logical AND of multiple filters.
184    And(&'a [PooledFilter<'a>]),
185    /// Logical OR of multiple filters.
186    Or(&'a [PooledFilter<'a>]),
187    /// Logical NOT of a filter.
188    Not(&'a PooledFilter<'a>),
189}
190
191impl<'a> PooledFilter<'a> {
192    /// Materialize the pooled filter into an owned Filter.
193    ///
194    /// This copies all data from the arena into owned allocations.
195    pub fn materialize(&self) -> Filter {
196        match self {
197            PooledFilter::None => Filter::None,
198            PooledFilter::Equals(field, value) => {
199                Filter::Equals(Cow::Owned((*field).to_string()), value.materialize())
200            }
201            PooledFilter::NotEquals(field, value) => {
202                Filter::NotEquals(Cow::Owned((*field).to_string()), value.materialize())
203            }
204            PooledFilter::Lt(field, value) => {
205                Filter::Lt(Cow::Owned((*field).to_string()), value.materialize())
206            }
207            PooledFilter::Lte(field, value) => {
208                Filter::Lte(Cow::Owned((*field).to_string()), value.materialize())
209            }
210            PooledFilter::Gt(field, value) => {
211                Filter::Gt(Cow::Owned((*field).to_string()), value.materialize())
212            }
213            PooledFilter::Gte(field, value) => {
214                Filter::Gte(Cow::Owned((*field).to_string()), value.materialize())
215            }
216            PooledFilter::In(field, values) => {
217                Filter::In(
218                    Cow::Owned((*field).to_string()),
219                    values.iter().map(|v| v.materialize()).collect(),
220                )
221            }
222            PooledFilter::NotIn(field, values) => {
223                Filter::NotIn(
224                    Cow::Owned((*field).to_string()),
225                    values.iter().map(|v| v.materialize()).collect(),
226                )
227            }
228            PooledFilter::Contains(field, value) => {
229                Filter::Contains(Cow::Owned((*field).to_string()), value.materialize())
230            }
231            PooledFilter::StartsWith(field, value) => {
232                Filter::StartsWith(Cow::Owned((*field).to_string()), value.materialize())
233            }
234            PooledFilter::EndsWith(field, value) => {
235                Filter::EndsWith(Cow::Owned((*field).to_string()), value.materialize())
236            }
237            PooledFilter::IsNull(field) => Filter::IsNull(Cow::Owned((*field).to_string())),
238            PooledFilter::IsNotNull(field) => Filter::IsNotNull(Cow::Owned((*field).to_string())),
239            PooledFilter::And(filters) => {
240                Filter::And(filters.iter().map(|f| f.materialize()).collect::<Vec<_>>().into_boxed_slice())
241            }
242            PooledFilter::Or(filters) => {
243                Filter::Or(filters.iter().map(|f| f.materialize()).collect::<Vec<_>>().into_boxed_slice())
244            }
245            PooledFilter::Not(filter) => Filter::Not(Box::new(filter.materialize())),
246        }
247    }
248}
249
250/// A filter value that lives in the pool's arena.
251#[derive(Debug, Clone, Copy)]
252pub enum PooledValue<'a> {
253    /// Null value.
254    Null,
255    /// Boolean value.
256    Bool(bool),
257    /// Integer value.
258    Int(i64),
259    /// Float value.
260    Float(f64),
261    /// String value (borrowed from arena).
262    String(&'a str),
263    /// JSON value (borrowed from arena).
264    Json(&'a str),
265}
266
267impl<'a> PooledValue<'a> {
268    /// Materialize the pooled value into an owned FilterValue.
269    pub fn materialize(&self) -> FilterValue {
270        match self {
271            PooledValue::Null => FilterValue::Null,
272            PooledValue::Bool(b) => FilterValue::Bool(*b),
273            PooledValue::Int(i) => FilterValue::Int(*i),
274            PooledValue::Float(f) => FilterValue::Float(*f),
275            PooledValue::String(s) => FilterValue::String((*s).to_string()),
276            PooledValue::Json(s) => FilterValue::Json(serde_json::from_str(s).unwrap_or_default()),
277        }
278    }
279}
280
281/// A builder for constructing filters within a pool.
282///
283/// Provides ergonomic methods for building filter trees with minimal allocations.
284pub struct FilterBuilder<'a> {
285    arena: &'a Bump,
286}
287
288impl<'a> FilterBuilder<'a> {
289    fn new(arena: &'a Bump) -> Self {
290        Self { arena }
291    }
292
293    /// Create a pooled string from a string slice.
294    fn alloc_str(&self, s: &str) -> &'a str {
295        self.arena.alloc_str(s)
296    }
297
298    /// Create a pooled slice from a vector of pooled filters.
299    fn alloc_filters(&self, filters: Vec<PooledFilter<'a>>) -> &'a [PooledFilter<'a>] {
300        self.arena.alloc_slice_fill_iter(filters)
301    }
302
303    /// Create a pooled slice from a vector of pooled values.
304    fn alloc_values(&self, values: Vec<PooledValue<'a>>) -> &'a [PooledValue<'a>] {
305        self.arena.alloc_slice_fill_iter(values)
306    }
307
308    /// Convert a value into a pooled value.
309    pub fn value<V: IntoPooledValue<'a>>(&self, v: V) -> PooledValue<'a> {
310        v.into_pooled(self)
311    }
312
313    /// Create an empty filter (matches everything).
314    pub fn none(&self) -> PooledFilter<'a> {
315        PooledFilter::None
316    }
317
318    /// Create an equals filter.
319    ///
320    /// # Examples
321    ///
322    /// ```rust
323    /// use prax_query::pool::FilterPool;
324    ///
325    /// let pool = FilterPool::new();
326    /// let filter = pool.build(|b| b.eq("status", "active"));
327    /// ```
328    pub fn eq<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
329        PooledFilter::Equals(self.alloc_str(field), value.into_pooled(self))
330    }
331
332    /// Create a not equals filter.
333    pub fn ne<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
334        PooledFilter::NotEquals(self.alloc_str(field), value.into_pooled(self))
335    }
336
337    /// Create a less than filter.
338    pub fn lt<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
339        PooledFilter::Lt(self.alloc_str(field), value.into_pooled(self))
340    }
341
342    /// Create a less than or equal filter.
343    pub fn lte<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
344        PooledFilter::Lte(self.alloc_str(field), value.into_pooled(self))
345    }
346
347    /// Create a greater than filter.
348    pub fn gt<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
349        PooledFilter::Gt(self.alloc_str(field), value.into_pooled(self))
350    }
351
352    /// Create a greater than or equal filter.
353    pub fn gte<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
354        PooledFilter::Gte(self.alloc_str(field), value.into_pooled(self))
355    }
356
357    /// Create an IN filter.
358    ///
359    /// # Examples
360    ///
361    /// ```rust
362    /// use prax_query::pool::FilterPool;
363    ///
364    /// let pool = FilterPool::new();
365    /// let filter = pool.build(|b| {
366    ///     b.is_in("status", vec![b.value("pending"), b.value("processing")])
367    /// });
368    /// ```
369    pub fn is_in(&self, field: &str, values: Vec<PooledValue<'a>>) -> PooledFilter<'a> {
370        PooledFilter::In(self.alloc_str(field), self.alloc_values(values))
371    }
372
373    /// Create a NOT IN filter.
374    pub fn not_in(&self, field: &str, values: Vec<PooledValue<'a>>) -> PooledFilter<'a> {
375        PooledFilter::NotIn(self.alloc_str(field), self.alloc_values(values))
376    }
377
378    /// Create a contains filter (LIKE %value%).
379    pub fn contains<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
380        PooledFilter::Contains(self.alloc_str(field), value.into_pooled(self))
381    }
382
383    /// Create a starts with filter (LIKE value%).
384    pub fn starts_with<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
385        PooledFilter::StartsWith(self.alloc_str(field), value.into_pooled(self))
386    }
387
388    /// Create an ends with filter (LIKE %value).
389    pub fn ends_with<V: IntoPooledValue<'a>>(&self, field: &str, value: V) -> PooledFilter<'a> {
390        PooledFilter::EndsWith(self.alloc_str(field), value.into_pooled(self))
391    }
392
393    /// Create an IS NULL filter.
394    pub fn is_null(&self, field: &str) -> PooledFilter<'a> {
395        PooledFilter::IsNull(self.alloc_str(field))
396    }
397
398    /// Create an IS NOT NULL filter.
399    pub fn is_not_null(&self, field: &str) -> PooledFilter<'a> {
400        PooledFilter::IsNotNull(self.alloc_str(field))
401    }
402
403    /// Create an AND filter combining multiple filters.
404    ///
405    /// # Examples
406    ///
407    /// ```rust
408    /// use prax_query::pool::FilterPool;
409    ///
410    /// let pool = FilterPool::new();
411    /// let filter = pool.build(|b| {
412    ///     b.and(vec![
413    ///         b.eq("active", true),
414    ///         b.gt("score", 100),
415    ///         b.is_not_null("email"),
416    ///     ])
417    /// });
418    /// ```
419    pub fn and(&self, filters: Vec<PooledFilter<'a>>) -> PooledFilter<'a> {
420        // Filter out None filters
421        let filters: Vec<_> = filters
422            .into_iter()
423            .filter(|f| !matches!(f, PooledFilter::None))
424            .collect();
425
426        match filters.len() {
427            0 => PooledFilter::None,
428            1 => filters.into_iter().next().unwrap(),
429            _ => PooledFilter::And(self.alloc_filters(filters)),
430        }
431    }
432
433    /// Create an OR filter combining multiple filters.
434    ///
435    /// # Examples
436    ///
437    /// ```rust
438    /// use prax_query::pool::FilterPool;
439    ///
440    /// let pool = FilterPool::new();
441    /// let filter = pool.build(|b| {
442    ///     b.or(vec![
443    ///         b.eq("role", "admin"),
444    ///         b.eq("role", "moderator"),
445    ///     ])
446    /// });
447    /// ```
448    pub fn or(&self, filters: Vec<PooledFilter<'a>>) -> PooledFilter<'a> {
449        // Filter out None filters
450        let filters: Vec<_> = filters
451            .into_iter()
452            .filter(|f| !matches!(f, PooledFilter::None))
453            .collect();
454
455        match filters.len() {
456            0 => PooledFilter::None,
457            1 => filters.into_iter().next().unwrap(),
458            _ => PooledFilter::Or(self.alloc_filters(filters)),
459        }
460    }
461
462    /// Create a NOT filter.
463    ///
464    /// # Examples
465    ///
466    /// ```rust
467    /// use prax_query::pool::FilterPool;
468    ///
469    /// let pool = FilterPool::new();
470    /// let filter = pool.build(|b| b.not(b.eq("deleted", true)));
471    /// ```
472    pub fn not(&self, filter: PooledFilter<'a>) -> PooledFilter<'a> {
473        if matches!(filter, PooledFilter::None) {
474            return PooledFilter::None;
475        }
476        PooledFilter::Not(self.arena.alloc(filter))
477    }
478}
479
480/// Trait for types that can be converted to a pooled value.
481pub trait IntoPooledValue<'a> {
482    fn into_pooled(self, builder: &FilterBuilder<'a>) -> PooledValue<'a>;
483}
484
485impl<'a> IntoPooledValue<'a> for bool {
486    fn into_pooled(self, _builder: &FilterBuilder<'a>) -> PooledValue<'a> {
487        PooledValue::Bool(self)
488    }
489}
490
491impl<'a> IntoPooledValue<'a> for i32 {
492    fn into_pooled(self, _builder: &FilterBuilder<'a>) -> PooledValue<'a> {
493        PooledValue::Int(self as i64)
494    }
495}
496
497impl<'a> IntoPooledValue<'a> for i64 {
498    fn into_pooled(self, _builder: &FilterBuilder<'a>) -> PooledValue<'a> {
499        PooledValue::Int(self)
500    }
501}
502
503impl<'a> IntoPooledValue<'a> for f64 {
504    fn into_pooled(self, _builder: &FilterBuilder<'a>) -> PooledValue<'a> {
505        PooledValue::Float(self)
506    }
507}
508
509impl<'a> IntoPooledValue<'a> for &str {
510    fn into_pooled(self, builder: &FilterBuilder<'a>) -> PooledValue<'a> {
511        PooledValue::String(builder.alloc_str(self))
512    }
513}
514
515impl<'a> IntoPooledValue<'a> for String {
516    fn into_pooled(self, builder: &FilterBuilder<'a>) -> PooledValue<'a> {
517        PooledValue::String(builder.alloc_str(&self))
518    }
519}
520
521impl<'a> IntoPooledValue<'a> for PooledValue<'a> {
522    fn into_pooled(self, _builder: &FilterBuilder<'a>) -> PooledValue<'a> {
523        self
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_pool_basic_filter() {
533        let pool = FilterPool::new();
534        let filter = pool.build(|b| b.eq("id", 42));
535
536        assert!(matches!(filter, Filter::Equals(_, _)));
537    }
538
539    #[test]
540    fn test_pool_and_filter() {
541        let pool = FilterPool::new();
542        let filter = pool.build(|b| {
543            b.and(vec![
544                b.eq("active", true),
545                b.gt("score", 100),
546            ])
547        });
548
549        assert!(matches!(filter, Filter::And(_)));
550    }
551
552    #[test]
553    fn test_pool_or_filter() {
554        let pool = FilterPool::new();
555        let filter = pool.build(|b| {
556            b.or(vec![
557                b.eq("status", "pending"),
558                b.eq("status", "processing"),
559            ])
560        });
561
562        assert!(matches!(filter, Filter::Or(_)));
563    }
564
565    #[test]
566    fn test_pool_nested_filter() {
567        let pool = FilterPool::new();
568        let filter = pool.build(|b| {
569            b.and(vec![
570                b.eq("active", true),
571                b.or(vec![
572                    b.gt("age", 18),
573                    b.eq("verified", true),
574                ]),
575                b.not(b.eq("deleted", true)),
576            ])
577        });
578
579        assert!(matches!(filter, Filter::And(_)));
580    }
581
582    #[test]
583    fn test_pool_in_filter() {
584        let pool = FilterPool::new();
585        let filter = pool.build(|b| {
586            b.is_in("status", vec![
587                b.value("pending"),
588                b.value("processing"),
589                b.value("completed"),
590            ])
591        });
592
593        assert!(matches!(filter, Filter::In(_, _)));
594    }
595
596    #[test]
597    fn test_pool_reset() {
598        let mut pool = FilterPool::new();
599
600        // Build first filter
601        let _ = pool.build(|b| b.eq("id", 1));
602        let bytes1 = pool.allocated_bytes();
603
604        // Reset pool
605        pool.reset();
606
607        // Build second filter (should reuse memory)
608        let _ = pool.build(|b| b.eq("id", 2));
609        let bytes2 = pool.allocated_bytes();
610
611        // After reset, memory usage should be similar
612        assert!(bytes2 <= bytes1 * 2); // Allow some growth
613    }
614
615    #[test]
616    fn test_pool_empty_and() {
617        let pool = FilterPool::new();
618        let filter = pool.build(|b| b.and(vec![]));
619
620        assert!(matches!(filter, Filter::None));
621    }
622
623    #[test]
624    fn test_pool_single_and() {
625        let pool = FilterPool::new();
626        let filter = pool.build(|b| b.and(vec![b.eq("id", 1)]));
627
628        // Single element AND should be simplified
629        assert!(matches!(filter, Filter::Equals(_, _)));
630    }
631
632    #[test]
633    fn test_pool_null_filters() {
634        let pool = FilterPool::new();
635        let filter = pool.build(|b| b.is_null("deleted_at"));
636
637        assert!(matches!(filter, Filter::IsNull(_)));
638    }
639
640    #[test]
641    fn test_pool_deeply_nested() {
642        let pool = FilterPool::new();
643
644        // Build a deeply nested filter tree
645        let filter = pool.build(|b| {
646            b.and(vec![
647                b.or(vec![
648                    b.and(vec![
649                        b.eq("a", 1),
650                        b.eq("b", 2),
651                    ]),
652                    b.and(vec![
653                        b.eq("c", 3),
654                        b.eq("d", 4),
655                    ]),
656                ]),
657                b.not(b.or(vec![
658                    b.eq("e", 5),
659                    b.eq("f", 6),
660                ])),
661            ])
662        });
663
664        // Verify structure
665        assert!(matches!(filter, Filter::And(_)));
666
667        // Generate SQL to verify correctness
668        let (sql, params) = filter.to_sql(0);
669        assert!(sql.contains("AND"));
670        assert!(sql.contains("OR"));
671        assert!(sql.contains("NOT"));
672        assert_eq!(params.len(), 6);
673    }
674
675    #[test]
676    fn test_pool_string_values() {
677        let pool = FilterPool::new();
678        let filter = pool.build(|b| {
679            b.and(vec![
680                b.eq("name", "Alice"),
681                b.contains("email", "@example.com"),
682                b.starts_with("phone", "+1"),
683            ])
684        });
685
686        let (sql, params) = filter.to_sql(0);
687        assert!(sql.contains("LIKE"));
688        assert_eq!(params.len(), 3);
689    }
690}
691