prax_query/
static_filter.rs

1//! Static filter construction for zero-allocation filters.
2//!
3//! This module provides zero-cost filter construction through:
4//! - Static field name constants
5//! - Type-level filter builders
6//! - Compile-time filter macros
7//!
8//! # Performance
9//!
10//! Static filters avoid heap allocations entirely:
11//! - Field names are `&'static str` (no `Cow` overhead)
12//! - Values are constructed inline
13//! - Common patterns are pre-computed
14//!
15//! # Examples
16//!
17//! ```rust
18//! use prax_query::static_filter::{StaticFilter, eq, gt, and2};
19//! use prax_query::static_filter::fields;
20//!
21//! // Zero-allocation filter construction
22//! let filter = eq(fields::ID, 42);
23//! let filter = gt(fields::AGE, 18);
24//!
25//! // Combine two filters (optimized path)
26//! let filter = and2(
27//!     eq(fields::ACTIVE, true),
28//!     gt(fields::SCORE, 100),
29//! );
30//! ```
31
32use crate::filter::{Filter, FilterValue, ValueList};
33use std::borrow::Cow;
34
35/// A static filter with compile-time known field name.
36///
37/// This is a zero-cost abstraction over `Filter` that avoids
38/// the `Cow<'static, str>` overhead for field names.
39#[derive(Debug, Clone, PartialEq)]
40pub struct StaticFilter {
41    inner: Filter,
42}
43
44impl StaticFilter {
45    /// Create a new static filter.
46    #[inline]
47    pub const fn new(inner: Filter) -> Self {
48        Self { inner }
49    }
50
51    /// Convert to the underlying Filter.
52    #[inline]
53    pub fn into_filter(self) -> Filter {
54        self.inner
55    }
56
57    /// Get a reference to the underlying Filter.
58    #[inline]
59    pub fn as_filter(&self) -> &Filter {
60        &self.inner
61    }
62}
63
64impl From<StaticFilter> for Filter {
65    #[inline]
66    fn from(f: StaticFilter) -> Self {
67        f.inner
68    }
69}
70
71/// Common field name constants for zero-allocation filters.
72///
73/// These are pre-defined `&'static str` values for common database columns.
74/// Using these avoids the overhead of `Cow::Borrowed` construction.
75pub mod fields {
76    /// Primary key field.
77    pub const ID: &str = "id";
78    /// UUID field.
79    pub const UUID: &str = "uuid";
80    /// Name field.
81    pub const NAME: &str = "name";
82    /// Email field.
83    pub const EMAIL: &str = "email";
84    /// Username field.
85    pub const USERNAME: &str = "username";
86    /// Password hash field.
87    pub const PASSWORD: &str = "password";
88    /// Title field.
89    pub const TITLE: &str = "title";
90    /// Description field.
91    pub const DESCRIPTION: &str = "description";
92    /// Content field.
93    pub const CONTENT: &str = "content";
94    /// Body field.
95    pub const BODY: &str = "body";
96    /// Status field.
97    pub const STATUS: &str = "status";
98    /// Type field.
99    pub const TYPE: &str = "type";
100    /// Role field.
101    pub const ROLE: &str = "role";
102    /// Active flag field.
103    pub const ACTIVE: &str = "active";
104    /// Enabled flag field.
105    pub const ENABLED: &str = "enabled";
106    /// Deleted flag field.
107    pub const DELETED: &str = "deleted";
108    /// Verified flag field.
109    pub const VERIFIED: &str = "verified";
110    /// Published flag field.
111    pub const PUBLISHED: &str = "published";
112    /// Count field.
113    pub const COUNT: &str = "count";
114    /// Score field.
115    pub const SCORE: &str = "score";
116    /// Priority field.
117    pub const PRIORITY: &str = "priority";
118    /// Order/sort order field.
119    pub const ORDER: &str = "order";
120    /// Position field.
121    pub const POSITION: &str = "position";
122    /// Age field.
123    pub const AGE: &str = "age";
124    /// Amount field.
125    pub const AMOUNT: &str = "amount";
126    /// Price field.
127    pub const PRICE: &str = "price";
128    /// Quantity field.
129    pub const QUANTITY: &str = "quantity";
130    /// Foreign key: user_id.
131    pub const USER_ID: &str = "user_id";
132    /// Foreign key: post_id.
133    pub const POST_ID: &str = "post_id";
134    /// Foreign key: comment_id.
135    pub const COMMENT_ID: &str = "comment_id";
136    /// Foreign key: category_id.
137    pub const CATEGORY_ID: &str = "category_id";
138    /// Foreign key: parent_id.
139    pub const PARENT_ID: &str = "parent_id";
140    /// Foreign key: author_id.
141    pub const AUTHOR_ID: &str = "author_id";
142    /// Foreign key: owner_id.
143    pub const OWNER_ID: &str = "owner_id";
144    /// Timestamp: created_at.
145    pub const CREATED_AT: &str = "created_at";
146    /// Timestamp: updated_at.
147    pub const UPDATED_AT: &str = "updated_at";
148    /// Timestamp: deleted_at.
149    pub const DELETED_AT: &str = "deleted_at";
150    /// Timestamp: published_at.
151    pub const PUBLISHED_AT: &str = "published_at";
152    /// Timestamp: expires_at.
153    pub const EXPIRES_AT: &str = "expires_at";
154    /// Timestamp: starts_at.
155    pub const STARTS_AT: &str = "starts_at";
156    /// Timestamp: ends_at.
157    pub const ENDS_AT: &str = "ends_at";
158    /// Timestamp: last_login_at.
159    pub const LAST_LOGIN_AT: &str = "last_login_at";
160    /// Timestamp: verified_at.
161    pub const VERIFIED_AT: &str = "verified_at";
162    /// Slug field.
163    pub const SLUG: &str = "slug";
164    /// URL field.
165    pub const URL: &str = "url";
166    /// Path field.
167    pub const PATH: &str = "path";
168    /// Key field.
169    pub const KEY: &str = "key";
170    /// Value field.
171    pub const VALUE: &str = "value";
172    /// Token field.
173    pub const TOKEN: &str = "token";
174    /// Code field.
175    pub const CODE: &str = "code";
176    /// Version field.
177    pub const VERSION: &str = "version";
178}
179
180// ============================================================================
181// Zero-allocation filter constructors
182// ============================================================================
183
184/// Create an equality filter with static field name.
185///
186/// This is the fastest way to create an equality filter - no heap allocation
187/// if the value is a primitive type.
188///
189/// # Example
190///
191/// ```rust
192/// use prax_query::static_filter::{eq, fields};
193///
194/// let filter = eq(fields::ID, 42);
195/// let filter = eq(fields::ACTIVE, true);
196/// let filter = eq(fields::NAME, "Alice");
197/// ```
198#[inline]
199pub fn eq(field: &'static str, value: impl Into<FilterValue>) -> Filter {
200    Filter::Equals(Cow::Borrowed(field), value.into())
201}
202
203/// Create a not-equals filter with static field name.
204#[inline]
205pub fn ne(field: &'static str, value: impl Into<FilterValue>) -> Filter {
206    Filter::NotEquals(Cow::Borrowed(field), value.into())
207}
208
209/// Create a less-than filter with static field name.
210#[inline]
211pub fn lt(field: &'static str, value: impl Into<FilterValue>) -> Filter {
212    Filter::Lt(Cow::Borrowed(field), value.into())
213}
214
215/// Create a less-than-or-equal filter with static field name.
216#[inline]
217pub fn lte(field: &'static str, value: impl Into<FilterValue>) -> Filter {
218    Filter::Lte(Cow::Borrowed(field), value.into())
219}
220
221/// Create a greater-than filter with static field name.
222#[inline]
223pub fn gt(field: &'static str, value: impl Into<FilterValue>) -> Filter {
224    Filter::Gt(Cow::Borrowed(field), value.into())
225}
226
227/// Create a greater-than-or-equal filter with static field name.
228#[inline]
229pub fn gte(field: &'static str, value: impl Into<FilterValue>) -> Filter {
230    Filter::Gte(Cow::Borrowed(field), value.into())
231}
232
233/// Create an IS NULL filter with static field name.
234#[inline]
235pub const fn is_null(field: &'static str) -> Filter {
236    Filter::IsNull(Cow::Borrowed(field))
237}
238
239/// Create an IS NOT NULL filter with static field name.
240#[inline]
241pub const fn is_not_null(field: &'static str) -> Filter {
242    Filter::IsNotNull(Cow::Borrowed(field))
243}
244
245/// Create a LIKE %value% filter with static field name.
246#[inline]
247pub fn contains(field: &'static str, value: impl Into<FilterValue>) -> Filter {
248    Filter::Contains(Cow::Borrowed(field), value.into())
249}
250
251/// Create a LIKE value% filter with static field name.
252#[inline]
253pub fn starts_with(field: &'static str, value: impl Into<FilterValue>) -> Filter {
254    Filter::StartsWith(Cow::Borrowed(field), value.into())
255}
256
257/// Create a LIKE %value filter with static field name.
258#[inline]
259pub fn ends_with(field: &'static str, value: impl Into<FilterValue>) -> Filter {
260    Filter::EndsWith(Cow::Borrowed(field), value.into())
261}
262
263/// Create an IN filter with static field name.
264#[inline]
265pub fn in_list(field: &'static str, values: impl Into<ValueList>) -> Filter {
266    Filter::In(Cow::Borrowed(field), values.into())
267}
268
269/// Create a NOT IN filter with static field name.
270#[inline]
271pub fn not_in_list(field: &'static str, values: impl Into<ValueList>) -> Filter {
272    Filter::NotIn(Cow::Borrowed(field), values.into())
273}
274
275// ============================================================================
276// Optimized combinators for small filter counts
277// ============================================================================
278
279/// Combine exactly 2 filters with AND (optimized, avoids vec allocation overhead).
280#[inline]
281pub fn and2(a: Filter, b: Filter) -> Filter {
282    Filter::And(Box::new([a, b]))
283}
284
285/// Combine exactly 3 filters with AND.
286#[inline]
287pub fn and3(a: Filter, b: Filter, c: Filter) -> Filter {
288    Filter::And(Box::new([a, b, c]))
289}
290
291/// Combine exactly 4 filters with AND.
292#[inline]
293pub fn and4(a: Filter, b: Filter, c: Filter, d: Filter) -> Filter {
294    Filter::And(Box::new([a, b, c, d]))
295}
296
297/// Combine exactly 5 filters with AND.
298#[inline]
299pub fn and5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Filter {
300    Filter::And(Box::new([a, b, c, d, e]))
301}
302
303/// Combine exactly 2 filters with OR (optimized, avoids vec allocation overhead).
304#[inline]
305pub fn or2(a: Filter, b: Filter) -> Filter {
306    Filter::Or(Box::new([a, b]))
307}
308
309/// Combine exactly 3 filters with OR.
310#[inline]
311pub fn or3(a: Filter, b: Filter, c: Filter) -> Filter {
312    Filter::Or(Box::new([a, b, c]))
313}
314
315/// Combine exactly 4 filters with OR.
316#[inline]
317pub fn or4(a: Filter, b: Filter, c: Filter, d: Filter) -> Filter {
318    Filter::Or(Box::new([a, b, c, d]))
319}
320
321/// Combine exactly 5 filters with OR.
322#[inline]
323pub fn or5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Filter {
324    Filter::Or(Box::new([a, b, c, d, e]))
325}
326
327/// Negate a filter.
328#[inline]
329pub fn not(filter: Filter) -> Filter {
330    Filter::Not(Box::new(filter))
331}
332
333// ============================================================================
334// Compact filter value types
335// ============================================================================
336
337/// A compact filter value optimized for common cases.
338///
339/// Uses a tagged union representation to minimize size:
340/// - Discriminant is inline with data
341/// - Small strings can be stored inline (future optimization)
342#[derive(Debug, Clone, PartialEq)]
343#[repr(u8)]
344pub enum CompactValue {
345    /// Null value.
346    Null = 0,
347    /// Boolean true.
348    True = 1,
349    /// Boolean false.
350    False = 2,
351    /// Small integer (-128 to 127).
352    SmallInt(i8) = 3,
353    /// Full integer.
354    Int(i64) = 4,
355    /// Float value.
356    Float(f64) = 5,
357    /// String value.
358    String(String) = 6,
359}
360
361impl CompactValue {
362    /// Convert to a FilterValue.
363    #[inline]
364    pub fn into_filter_value(self) -> FilterValue {
365        match self {
366            Self::Null => FilterValue::Null,
367            Self::True => FilterValue::Bool(true),
368            Self::False => FilterValue::Bool(false),
369            Self::SmallInt(v) => FilterValue::Int(v as i64),
370            Self::Int(v) => FilterValue::Int(v),
371            Self::Float(v) => FilterValue::Float(v),
372            Self::String(v) => FilterValue::String(v),
373        }
374    }
375}
376
377impl From<bool> for CompactValue {
378    #[inline]
379    fn from(v: bool) -> Self {
380        if v { Self::True } else { Self::False }
381    }
382}
383
384impl From<i32> for CompactValue {
385    #[inline]
386    fn from(v: i32) -> Self {
387        if (-128..=127).contains(&v) {
388            Self::SmallInt(v as i8)
389        } else {
390            Self::Int(v as i64)
391        }
392    }
393}
394
395impl From<i64> for CompactValue {
396    #[inline]
397    fn from(v: i64) -> Self {
398        if (-128..=127).contains(&v) {
399            Self::SmallInt(v as i8)
400        } else {
401            Self::Int(v)
402        }
403    }
404}
405
406impl From<f64> for CompactValue {
407    #[inline]
408    fn from(v: f64) -> Self {
409        Self::Float(v)
410    }
411}
412
413impl From<String> for CompactValue {
414    #[inline]
415    fn from(v: String) -> Self {
416        Self::String(v)
417    }
418}
419
420impl From<&str> for CompactValue {
421    #[inline]
422    fn from(v: &str) -> Self {
423        Self::String(v.to_string())
424    }
425}
426
427impl From<CompactValue> for FilterValue {
428    #[inline]
429    fn from(v: CompactValue) -> Self {
430        v.into_filter_value()
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_eq_filter() {
440        let filter = eq(fields::ID, 42);
441        assert!(matches!(filter, Filter::Equals(_, FilterValue::Int(42))));
442    }
443
444    #[test]
445    fn test_gt_filter() {
446        let filter = gt(fields::AGE, 18);
447        assert!(matches!(filter, Filter::Gt(_, FilterValue::Int(18))));
448    }
449
450    #[test]
451    fn test_is_null_filter() {
452        let filter = is_null(fields::DELETED_AT);
453        assert!(matches!(filter, Filter::IsNull(_)));
454    }
455
456    #[test]
457    fn test_and2_filter() {
458        let filter = and2(eq(fields::ACTIVE, true), gt(fields::SCORE, 100));
459        assert!(matches!(filter, Filter::And(_)));
460    }
461
462    #[test]
463    fn test_or2_filter() {
464        let filter = or2(eq(fields::STATUS, "active"), eq(fields::STATUS, "pending"));
465        assert!(matches!(filter, Filter::Or(_)));
466    }
467
468    #[test]
469    fn test_compact_value_bool() {
470        let v: CompactValue = true.into();
471        assert!(matches!(v, CompactValue::True));
472        assert_eq!(v.into_filter_value(), FilterValue::Bool(true));
473    }
474
475    #[test]
476    fn test_compact_value_small_int() {
477        let v: CompactValue = 42i32.into();
478        assert!(matches!(v, CompactValue::SmallInt(42)));
479    }
480
481    #[test]
482    fn test_compact_value_large_int() {
483        let v: CompactValue = 1000i32.into();
484        assert!(matches!(v, CompactValue::Int(1000)));
485    }
486
487    #[test]
488    fn test_field_constants() {
489        assert_eq!(fields::ID, "id");
490        assert_eq!(fields::EMAIL, "email");
491        assert_eq!(fields::CREATED_AT, "created_at");
492    }
493}