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