Skip to main content

prax_query/
filter.rs

1//! Filter types for building WHERE clauses.
2//!
3//! This module provides the building blocks for constructing type-safe query filters.
4//!
5//! # Performance
6//!
7//! Field names use `Cow<'static, str>` for optimal performance:
8//! - Static strings (`&'static str`) are borrowed with zero allocation
9//! - Dynamic strings are stored as owned `String`
10//! - Use `.into()` from static strings for best performance
11//!
12//! # Examples
13//!
14//! ## Basic Filters
15//!
16//! ```rust
17//! use prax_query::filter::{Filter, FilterValue};
18//!
19//! // Equality filter - zero allocation with static str
20//! let filter = Filter::Equals("id".into(), FilterValue::Int(42));
21//!
22//! // String contains
23//! let filter = Filter::Contains("email".into(), FilterValue::String("@example.com".into()));
24//!
25//! // Greater than
26//! let filter = Filter::Gt("age".into(), FilterValue::Int(18));
27//! ```
28//!
29//! ## Combining Filters
30//!
31//! ```rust
32//! use prax_query::filter::{Filter, FilterValue};
33//!
34//! // AND combination - use Filter::and() for convenience
35//! let filter = Filter::and([
36//!     Filter::Equals("active".into(), FilterValue::Bool(true)),
37//!     Filter::Gt("score".into(), FilterValue::Int(100)),
38//! ]);
39//!
40//! // OR combination - use Filter::or() for convenience
41//! let filter = Filter::or([
42//!     Filter::Equals("status".into(), FilterValue::String("pending".into())),
43//!     Filter::Equals("status".into(), FilterValue::String("processing".into())),
44//! ]);
45//!
46//! // NOT
47//! let filter = Filter::Not(Box::new(
48//!     Filter::Equals("deleted".into(), FilterValue::Bool(true))
49//! ));
50//! ```
51//!
52//! ## Null Checks
53//!
54//! ```rust
55//! use prax_query::filter::{Filter, FilterValue};
56//!
57//! // Is null
58//! let filter = Filter::IsNull("deleted_at".into());
59//!
60//! // Is not null
61//! let filter = Filter::IsNotNull("verified_at".into());
62//! ```
63
64use serde::{Deserialize, Serialize};
65use smallvec::SmallVec;
66use std::borrow::Cow;
67use tracing::debug;
68
69/// A list of filter values for IN/NOT IN clauses.
70///
71/// Uses `Vec<FilterValue>` for minimal Filter enum size (~64 bytes).
72/// This prioritizes cache efficiency over avoiding small allocations.
73///
74/// # Performance
75///
76/// While this does allocate for IN clauses, the benefits are:
77/// - Filter enum fits in a single cache line (64 bytes)
78/// - Better memory locality for filter iteration
79/// - Smaller stack usage for complex queries
80///
81/// For truly allocation-free IN filters, use `Filter::in_static()` with
82/// a static slice reference.
83pub type ValueList = Vec<FilterValue>;
84
85/// SmallVec-based value list for hot paths where small IN clauses are common.
86/// Use this explicitly when you know IN clauses are small (≤8 elements).
87pub type SmallValueList = SmallVec<[FilterValue; 8]>;
88
89/// Large value list type with 32 elements inline.
90/// Use this for known large IN clauses (e.g., batch operations).
91pub type LargeValueList = SmallVec<[FilterValue; 32]>;
92
93/// A field name that can be either a static string (zero allocation) or an owned string.
94///
95/// Uses `Cow<'static, str>` for optimal performance:
96/// - Static strings are borrowed without allocation
97/// - Dynamic strings are stored as owned `String`
98///
99/// # Examples
100///
101/// ```rust
102/// use prax_query::FieldName;
103///
104/// // Static strings - zero allocation (Cow::Borrowed)
105/// let name: FieldName = "id".into();
106/// let name: FieldName = "email".into();
107/// let name: FieldName = "user_id".into();
108/// let name: FieldName = "created_at".into();
109///
110/// // Dynamic strings work too (Cow::Owned)
111/// let name: FieldName = format!("field_{}", 1).into();
112/// ```
113pub type FieldName = Cow<'static, str>;
114
115/// A filter value that can be used in comparisons.
116///
117/// # Examples
118///
119/// ```rust
120/// use prax_query::FilterValue;
121///
122/// // From integers
123/// let val: FilterValue = 42.into();
124/// let val: FilterValue = 42i64.into();
125///
126/// // From strings
127/// let val: FilterValue = "hello".into();
128/// let val: FilterValue = String::from("world").into();
129///
130/// // From booleans
131/// let val: FilterValue = true.into();
132///
133/// // From floats
134/// let val: FilterValue = 3.14f64.into();
135///
136/// // Null value
137/// let val = FilterValue::Null;
138///
139/// // From vectors
140/// let val: FilterValue = vec![1, 2, 3].into();
141///
142/// // From Option (Some becomes value, None becomes Null)
143/// let val: FilterValue = Some(42).into();
144/// let val: FilterValue = Option::<i32>::None.into();
145/// assert!(val.is_null());
146/// ```
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148#[serde(untagged)]
149pub enum FilterValue {
150    /// Null value.
151    Null,
152    /// Boolean value.
153    Bool(bool),
154    /// Integer value.
155    Int(i64),
156    /// Float value.
157    Float(f64),
158    /// String value.
159    String(String),
160    /// JSON value.
161    Json(serde_json::Value),
162    /// List of values.
163    List(Vec<FilterValue>),
164}
165
166impl FilterValue {
167    /// Check if this is a null value.
168    pub fn is_null(&self) -> bool {
169        matches!(self, Self::Null)
170    }
171}
172
173impl From<bool> for FilterValue {
174    fn from(v: bool) -> Self {
175        Self::Bool(v)
176    }
177}
178
179impl From<i32> for FilterValue {
180    fn from(v: i32) -> Self {
181        Self::Int(v as i64)
182    }
183}
184
185impl From<i64> for FilterValue {
186    fn from(v: i64) -> Self {
187        Self::Int(v)
188    }
189}
190
191impl From<f64> for FilterValue {
192    fn from(v: f64) -> Self {
193        Self::Float(v)
194    }
195}
196
197impl From<String> for FilterValue {
198    fn from(v: String) -> Self {
199        Self::String(v)
200    }
201}
202
203impl From<&str> for FilterValue {
204    fn from(v: &str) -> Self {
205        Self::String(v.to_string())
206    }
207}
208
209impl<T: Into<FilterValue>> From<Vec<T>> for FilterValue {
210    fn from(v: Vec<T>) -> Self {
211        Self::List(v.into_iter().map(Into::into).collect())
212    }
213}
214
215impl<T: Into<FilterValue>> From<Option<T>> for FilterValue {
216    fn from(v: Option<T>) -> Self {
217        match v {
218            Some(v) => v.into(),
219            None => Self::Null,
220        }
221    }
222}
223
224// Integer widenings. The derive macro's `TypeCategory::Numeric` bucket
225// emits `.gt(v)` / `.in_(vec![v])` etc. on every Rust integer type, which
226// then flows through `v.into()` to a `FilterValue::Int(i64)`. Without
227// these impls, calling `user::age::gt(18u32)` would fail to compile.
228
229impl From<i8> for FilterValue {
230    fn from(v: i8) -> Self {
231        Self::Int(v as i64)
232    }
233}
234impl From<i16> for FilterValue {
235    fn from(v: i16) -> Self {
236        Self::Int(v as i64)
237    }
238}
239impl From<u8> for FilterValue {
240    fn from(v: u8) -> Self {
241        Self::Int(v as i64)
242    }
243}
244impl From<u16> for FilterValue {
245    fn from(v: u16) -> Self {
246        Self::Int(v as i64)
247    }
248}
249impl From<u32> for FilterValue {
250    fn from(v: u32) -> Self {
251        Self::Int(v as i64)
252    }
253}
254// u64 can exceed i64::MAX; panic on overflow rather than silently
255// clamping. Silent clamping lets a filter like `user::id::equals(u64::MAX)`
256// match the wrong row (`id = i64::MAX`) — a known authorization-bypass
257// footgun. Callers with known-safe values should cast explicitly:
258// `FilterValue::from(v as i64)`.
259impl From<u64> for FilterValue {
260    fn from(v: u64) -> Self {
261        let v = i64::try_from(v).expect(
262            "u64 value exceeds i64::MAX; cast explicitly to i64 or use FilterValue::String",
263        );
264        Self::Int(v)
265    }
266}
267
268impl From<f32> for FilterValue {
269    fn from(v: f32) -> Self {
270        Self::Float(f64::from(v))
271    }
272}
273
274// Temporal and UUID types round-trip as strings — every driver's row
275// bridge already materializes them via `FilterValue::String` (see
276// `MysqlRowRef`, `SqliteRowRef`, `MssqlRowRef`), so a matching pair on
277// the parameter-binding side keeps the derive's emitted
278// `user::when::gt(dt)` chain compiling and symmetric.
279//
280// Temporal values round-trip as RFC3339/ISO-8601 strings.
281// Microsecond precision matches what Postgres/MySQL store and what the driver
282// `RowRef` bridges read. Callers that need different precision or format
283// should build their own `FilterValue::String` value.
284
285impl From<chrono::DateTime<chrono::Utc>> for FilterValue {
286    fn from(v: chrono::DateTime<chrono::Utc>) -> Self {
287        // RFC3339 with microsecond precision: 2020-01-15T10:30:00.000000Z
288        Self::String(v.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
289    }
290}
291impl From<chrono::NaiveDateTime> for FilterValue {
292    fn from(v: chrono::NaiveDateTime) -> Self {
293        // ISO-8601 without timezone. Six fractional-second digits for
294        // bit-parity with Postgres/MySQL microsecond storage.
295        Self::String(v.format("%Y-%m-%dT%H:%M:%S%.6f").to_string())
296    }
297}
298impl From<chrono::NaiveDate> for FilterValue {
299    fn from(v: chrono::NaiveDate) -> Self {
300        Self::String(v.format("%Y-%m-%d").to_string())
301    }
302}
303impl From<chrono::NaiveTime> for FilterValue {
304    fn from(v: chrono::NaiveTime) -> Self {
305        Self::String(v.format("%H:%M:%S%.6f").to_string())
306    }
307}
308
309impl From<uuid::Uuid> for FilterValue {
310    fn from(v: uuid::Uuid) -> Self {
311        Self::String(v.to_string())
312    }
313}
314
315impl From<rust_decimal::Decimal> for FilterValue {
316    fn from(v: rust_decimal::Decimal) -> Self {
317        Self::String(v.to_string())
318    }
319}
320
321impl From<serde_json::Value> for FilterValue {
322    fn from(v: serde_json::Value) -> Self {
323        Self::Json(v)
324    }
325}
326
327// `Vec<u8>` is already reachable via the `Vec<T: Into<FilterValue>>`
328// blanket impl — it lands as `FilterValue::List` of `FilterValue::Int`
329// bytes. Drivers that want native BYTEA binding should intercept the
330// List variant and re-interpret. We intentionally don't shadow the
331// blanket with a dedicated impl (which would be a conflict anyway).
332
333/// Reverse of [`crate::row::FromColumn`]: convert an in-memory value to
334/// a [`FilterValue`] suitable for parameter binding.
335///
336/// Used by the relation executor and [`crate::traits::ModelWithPk`] to
337/// project a fetched row's primary/foreign key into a placeholder value
338/// without going through the `From<T>` path (which consumes the value).
339///
340/// # Intentional omissions
341///
342/// `u64` is omitted by design: [`From<u64>`] panics on overflow, but
343/// `to_filter_value(&self)` takes a borrow and cannot recover or fail
344/// gracefully without hidden clamping. Callers with `u64` primary keys
345/// should cast explicitly (`(self.id as i64).to_filter_value()`) or
346/// use `FilterValue::String(self.id.to_string())` when full range
347/// preservation matters.
348pub trait ToFilterValue {
349    /// Convert this value to a [`FilterValue`] by borrowing.
350    fn to_filter_value(&self) -> FilterValue;
351}
352
353impl ToFilterValue for i8 {
354    fn to_filter_value(&self) -> FilterValue {
355        FilterValue::Int(*self as i64)
356    }
357}
358impl ToFilterValue for i16 {
359    fn to_filter_value(&self) -> FilterValue {
360        FilterValue::Int(*self as i64)
361    }
362}
363impl ToFilterValue for i32 {
364    fn to_filter_value(&self) -> FilterValue {
365        FilterValue::Int(*self as i64)
366    }
367}
368impl ToFilterValue for i64 {
369    fn to_filter_value(&self) -> FilterValue {
370        FilterValue::Int(*self)
371    }
372}
373impl ToFilterValue for u8 {
374    fn to_filter_value(&self) -> FilterValue {
375        FilterValue::Int(*self as i64)
376    }
377}
378impl ToFilterValue for u16 {
379    fn to_filter_value(&self) -> FilterValue {
380        FilterValue::Int(*self as i64)
381    }
382}
383impl ToFilterValue for u32 {
384    fn to_filter_value(&self) -> FilterValue {
385        FilterValue::Int(*self as i64)
386    }
387}
388impl ToFilterValue for f32 {
389    fn to_filter_value(&self) -> FilterValue {
390        FilterValue::Float(f64::from(*self))
391    }
392}
393impl ToFilterValue for f64 {
394    fn to_filter_value(&self) -> FilterValue {
395        FilterValue::Float(*self)
396    }
397}
398impl ToFilterValue for bool {
399    fn to_filter_value(&self) -> FilterValue {
400        FilterValue::Bool(*self)
401    }
402}
403impl ToFilterValue for String {
404    fn to_filter_value(&self) -> FilterValue {
405        FilterValue::String(self.clone())
406    }
407}
408impl ToFilterValue for str {
409    fn to_filter_value(&self) -> FilterValue {
410        FilterValue::String(self.to_string())
411    }
412}
413impl ToFilterValue for uuid::Uuid {
414    fn to_filter_value(&self) -> FilterValue {
415        FilterValue::String(self.to_string())
416    }
417}
418impl ToFilterValue for rust_decimal::Decimal {
419    fn to_filter_value(&self) -> FilterValue {
420        FilterValue::String(self.to_string())
421    }
422}
423impl ToFilterValue for chrono::DateTime<chrono::Utc> {
424    fn to_filter_value(&self) -> FilterValue {
425        // Mirrors From<DateTime<Utc>>: RFC3339 with microsecond precision.
426        FilterValue::String(self.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
427    }
428}
429impl ToFilterValue for chrono::NaiveDateTime {
430    fn to_filter_value(&self) -> FilterValue {
431        FilterValue::String(self.format("%Y-%m-%dT%H:%M:%S%.6f").to_string())
432    }
433}
434impl ToFilterValue for chrono::NaiveDate {
435    fn to_filter_value(&self) -> FilterValue {
436        FilterValue::String(self.format("%Y-%m-%d").to_string())
437    }
438}
439impl ToFilterValue for chrono::NaiveTime {
440    fn to_filter_value(&self) -> FilterValue {
441        FilterValue::String(self.format("%H:%M:%S%.6f").to_string())
442    }
443}
444impl ToFilterValue for serde_json::Value {
445    fn to_filter_value(&self) -> FilterValue {
446        FilterValue::Json(self.clone())
447    }
448}
449impl ToFilterValue for Vec<u8> {
450    fn to_filter_value(&self) -> FilterValue {
451        // Bytes round-trip as a list of ints to match the existing
452        // `From<Vec<T>>` blanket behavior. Drivers that want native
453        // BYTEA binding intercept the List variant.
454        FilterValue::List(self.iter().map(|b| FilterValue::Int(*b as i64)).collect())
455    }
456}
457impl<T: ToFilterValue> ToFilterValue for Option<T> {
458    fn to_filter_value(&self) -> FilterValue {
459        self.as_ref()
460            .map(T::to_filter_value)
461            .unwrap_or(FilterValue::Null)
462    }
463}
464
465/// Scalar filter operations.
466#[derive(Debug, Clone, PartialEq)]
467pub enum ScalarFilter<T> {
468    /// Equals the value.
469    Equals(T),
470    /// Not equals the value.
471    Not(Box<T>),
472    /// In a list of values.
473    In(Vec<T>),
474    /// Not in a list of values.
475    NotIn(Vec<T>),
476    /// Less than.
477    Lt(T),
478    /// Less than or equal.
479    Lte(T),
480    /// Greater than.
481    Gt(T),
482    /// Greater than or equal.
483    Gte(T),
484    /// Contains (for strings).
485    Contains(T),
486    /// Starts with (for strings).
487    StartsWith(T),
488    /// Ends with (for strings).
489    EndsWith(T),
490    /// Is null.
491    IsNull,
492    /// Is not null.
493    IsNotNull,
494}
495
496impl<T: Into<FilterValue>> ScalarFilter<T> {
497    /// Convert to a Filter with the given column name.
498    ///
499    /// The column name can be a static string (zero allocation) or an owned string.
500    /// For IN/NOT IN filters, uses SmallVec to avoid heap allocation for ≤16 values.
501    pub fn into_filter(self, column: impl Into<FieldName>) -> Filter {
502        let column = column.into();
503        match self {
504            Self::Equals(v) => Filter::Equals(column, v.into()),
505            Self::Not(v) => Filter::NotEquals(column, (*v).into()),
506            Self::In(values) => Filter::In(column, values.into_iter().map(Into::into).collect()),
507            Self::NotIn(values) => {
508                Filter::NotIn(column, values.into_iter().map(Into::into).collect())
509            }
510            Self::Lt(v) => Filter::Lt(column, v.into()),
511            Self::Lte(v) => Filter::Lte(column, v.into()),
512            Self::Gt(v) => Filter::Gt(column, v.into()),
513            Self::Gte(v) => Filter::Gte(column, v.into()),
514            Self::Contains(v) => Filter::Contains(column, v.into()),
515            Self::StartsWith(v) => Filter::StartsWith(column, v.into()),
516            Self::EndsWith(v) => Filter::EndsWith(column, v.into()),
517            Self::IsNull => Filter::IsNull(column),
518            Self::IsNotNull => Filter::IsNotNull(column),
519        }
520    }
521}
522
523/// A complete filter that can be converted to SQL.
524///
525/// # Size Optimization
526///
527/// The Filter enum is designed to fit in a single cache line (~64 bytes):
528/// - Field names use `Cow<'static, str>` (24 bytes)
529/// - Filter values use `FilterValue` (40 bytes)
530/// - IN/NOT IN use `Vec<FilterValue>` (24 bytes) instead of SmallVec
531/// - AND/OR use `Box<[Filter]>` (16 bytes)
532///
533/// This enables efficient iteration and better CPU cache utilization.
534///
535/// # Zero-Allocation Patterns
536///
537/// For maximum performance, use static strings:
538/// ```rust
539/// use prax_query::filter::{Filter, FilterValue};
540/// // Zero allocation - static string borrowed
541/// let filter = Filter::Equals("id".into(), FilterValue::Int(42));
542/// ```
543#[derive(Debug, Clone, PartialEq)]
544#[repr(C)] // Ensure predictable memory layout
545#[derive(Default)]
546pub enum Filter {
547    /// No filter (always true).
548    #[default]
549    None,
550
551    /// Equals comparison.
552    Equals(FieldName, FilterValue),
553    /// Not equals comparison.
554    NotEquals(FieldName, FilterValue),
555
556    /// Less than comparison.
557    Lt(FieldName, FilterValue),
558    /// Less than or equal comparison.
559    Lte(FieldName, FilterValue),
560    /// Greater than comparison.
561    Gt(FieldName, FilterValue),
562    /// Greater than or equal comparison.
563    Gte(FieldName, FilterValue),
564
565    /// In a list of values.
566    In(FieldName, ValueList),
567    /// Not in a list of values.
568    NotIn(FieldName, ValueList),
569
570    /// Contains (LIKE %value%).
571    Contains(FieldName, FilterValue),
572    /// Starts with (LIKE value%).
573    StartsWith(FieldName, FilterValue),
574    /// Ends with (LIKE %value).
575    EndsWith(FieldName, FilterValue),
576
577    /// Is null check.
578    IsNull(FieldName),
579    /// Is not null check.
580    IsNotNull(FieldName),
581
582    /// Logical AND of multiple filters.
583    ///
584    /// Uses `Box<[Filter]>` instead of `Vec<Filter>` to save 8 bytes per filter
585    /// (no capacity field needed since filters are immutable after construction).
586    And(Box<[Filter]>),
587    /// Logical OR of multiple filters.
588    ///
589    /// Uses `Box<[Filter]>` instead of `Vec<Filter>` to save 8 bytes per filter
590    /// (no capacity field needed since filters are immutable after construction).
591    Or(Box<[Filter]>),
592    /// Logical NOT of a filter.
593    Not(Box<Filter>),
594}
595
596impl Filter {
597    /// Create an empty filter (matches everything).
598    #[inline(always)]
599    pub fn none() -> Self {
600        Self::None
601    }
602
603    /// Check if this filter is empty.
604    #[inline(always)]
605    pub fn is_none(&self) -> bool {
606        matches!(self, Self::None)
607    }
608
609    /// Create an AND filter from an iterator of filters.
610    ///
611    /// Automatically filters out `None` filters and simplifies single-element combinations.
612    ///
613    /// For known small counts, prefer `and2`, `and3`, `and5`, or `and_n` for better performance.
614    #[inline]
615    pub fn and(filters: impl IntoIterator<Item = Filter>) -> Self {
616        let filters: Vec<_> = filters.into_iter().filter(|f| !f.is_none()).collect();
617        let count = filters.len();
618        let result = match count {
619            0 => Self::None,
620            1 => filters.into_iter().next().unwrap(),
621            _ => Self::And(filters.into_boxed_slice()),
622        };
623        debug!(count, "Filter::and() created");
624        result
625    }
626
627    /// Create an AND filter from exactly two filters.
628    ///
629    /// More efficient than `and([a, b])` - avoids Vec allocation.
630    #[inline(always)]
631    pub fn and2(a: Filter, b: Filter) -> Self {
632        match (a.is_none(), b.is_none()) {
633            (true, true) => Self::None,
634            (true, false) => b,
635            (false, true) => a,
636            (false, false) => Self::And(Box::new([a, b])),
637        }
638    }
639
640    /// Create an OR filter from an iterator of filters.
641    ///
642    /// Automatically filters out `None` filters and simplifies single-element combinations.
643    ///
644    /// For known small counts, prefer `or2`, `or3`, or `or_n` for better performance.
645    #[inline]
646    pub fn or(filters: impl IntoIterator<Item = Filter>) -> Self {
647        let filters: Vec<_> = filters.into_iter().filter(|f| !f.is_none()).collect();
648        let count = filters.len();
649        let result = match count {
650            0 => Self::None,
651            1 => filters.into_iter().next().unwrap(),
652            _ => Self::Or(filters.into_boxed_slice()),
653        };
654        debug!(count, "Filter::or() created");
655        result
656    }
657
658    /// Create an OR filter from exactly two filters.
659    ///
660    /// More efficient than `or([a, b])` - avoids Vec allocation.
661    #[inline(always)]
662    pub fn or2(a: Filter, b: Filter) -> Self {
663        match (a.is_none(), b.is_none()) {
664            (true, true) => Self::None,
665            (true, false) => b,
666            (false, true) => a,
667            (false, false) => Self::Or(Box::new([a, b])),
668        }
669    }
670
671    // ========================================================================
672    // Const Generic Constructors (Zero Vec Allocation)
673    // ========================================================================
674
675    /// Create an AND filter from a fixed-size array (const generic).
676    ///
677    /// This is the most efficient way to create AND filters when the count is
678    /// known at compile time. It avoids Vec allocation entirely.
679    ///
680    /// # Examples
681    ///
682    /// ```rust
683    /// use prax_query::filter::{Filter, FilterValue};
684    ///
685    /// let filter = Filter::and_n([
686    ///     Filter::Equals("a".into(), FilterValue::Int(1)),
687    ///     Filter::Equals("b".into(), FilterValue::Int(2)),
688    ///     Filter::Equals("c".into(), FilterValue::Int(3)),
689    /// ]);
690    /// ```
691    #[inline(always)]
692    pub fn and_n<const N: usize>(filters: [Filter; N]) -> Self {
693        // Convert array to boxed slice directly (no Vec intermediate)
694        Self::And(Box::new(filters))
695    }
696
697    /// Create an OR filter from a fixed-size array (const generic).
698    ///
699    /// This is the most efficient way to create OR filters when the count is
700    /// known at compile time. It avoids Vec allocation entirely.
701    #[inline(always)]
702    pub fn or_n<const N: usize>(filters: [Filter; N]) -> Self {
703        Self::Or(Box::new(filters))
704    }
705
706    /// Create an AND filter from exactly 3 filters.
707    #[inline(always)]
708    pub fn and3(a: Filter, b: Filter, c: Filter) -> Self {
709        Self::And(Box::new([a, b, c]))
710    }
711
712    /// Create an AND filter from exactly 4 filters.
713    #[inline(always)]
714    pub fn and4(a: Filter, b: Filter, c: Filter, d: Filter) -> Self {
715        Self::And(Box::new([a, b, c, d]))
716    }
717
718    /// Create an AND filter from exactly 5 filters.
719    #[inline(always)]
720    pub fn and5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Self {
721        Self::And(Box::new([a, b, c, d, e]))
722    }
723
724    /// Create an OR filter from exactly 3 filters.
725    #[inline(always)]
726    pub fn or3(a: Filter, b: Filter, c: Filter) -> Self {
727        Self::Or(Box::new([a, b, c]))
728    }
729
730    /// Create an OR filter from exactly 4 filters.
731    #[inline(always)]
732    pub fn or4(a: Filter, b: Filter, c: Filter, d: Filter) -> Self {
733        Self::Or(Box::new([a, b, c, d]))
734    }
735
736    /// Create an OR filter from exactly 5 filters.
737    #[inline(always)]
738    pub fn or5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Self {
739        Self::Or(Box::new([a, b, c, d, e]))
740    }
741
742    // ========================================================================
743    // Optimized IN Filter Constructors
744    // ========================================================================
745
746    /// Create an IN filter from an iterator of i64 values.
747    ///
748    /// This is optimized for integer lists, avoiding the generic `Into<FilterValue>`
749    /// conversion overhead.
750    #[inline]
751    pub fn in_i64(field: impl Into<FieldName>, values: impl IntoIterator<Item = i64>) -> Self {
752        let list: ValueList = values.into_iter().map(FilterValue::Int).collect();
753        Self::In(field.into(), list)
754    }
755
756    /// Create an IN filter from an iterator of i32 values.
757    #[inline]
758    pub fn in_i32(field: impl Into<FieldName>, values: impl IntoIterator<Item = i32>) -> Self {
759        let list: ValueList = values
760            .into_iter()
761            .map(|v| FilterValue::Int(v as i64))
762            .collect();
763        Self::In(field.into(), list)
764    }
765
766    /// Create an IN filter from an iterator of string values.
767    #[inline]
768    pub fn in_strings(
769        field: impl Into<FieldName>,
770        values: impl IntoIterator<Item = String>,
771    ) -> Self {
772        let list: ValueList = values.into_iter().map(FilterValue::String).collect();
773        Self::In(field.into(), list)
774    }
775
776    /// Create an IN filter from a pre-built ValueList.
777    ///
778    /// Use this when you've already constructed a ValueList to avoid re-collection.
779    #[inline]
780    pub fn in_values(field: impl Into<FieldName>, values: ValueList) -> Self {
781        Self::In(field.into(), values)
782    }
783
784    /// Create an IN filter from a range of i64 values.
785    ///
786    /// Highly optimized for sequential integer ranges.
787    #[inline]
788    pub fn in_range(field: impl Into<FieldName>, range: std::ops::Range<i64>) -> Self {
789        let list: ValueList = range.map(FilterValue::Int).collect();
790        Self::In(field.into(), list)
791    }
792
793    /// Create an IN filter from a pre-allocated i64 slice with exact capacity.
794    ///
795    /// This is the most efficient way to create IN filters for i64 values
796    /// when you have a slice available.
797    #[inline(always)]
798    pub fn in_i64_slice(field: impl Into<FieldName>, values: &[i64]) -> Self {
799        let mut list = Vec::with_capacity(values.len());
800        for &v in values {
801            list.push(FilterValue::Int(v));
802        }
803        Self::In(field.into(), list)
804    }
805
806    /// Create an IN filter for i32 values from a slice.
807    #[inline(always)]
808    pub fn in_i32_slice(field: impl Into<FieldName>, values: &[i32]) -> Self {
809        let mut list = Vec::with_capacity(values.len());
810        for &v in values {
811            list.push(FilterValue::Int(v as i64));
812        }
813        Self::In(field.into(), list)
814    }
815
816    /// Create an IN filter for string values from a slice.
817    #[inline(always)]
818    pub fn in_str_slice(field: impl Into<FieldName>, values: &[&str]) -> Self {
819        let mut list = Vec::with_capacity(values.len());
820        for &v in values {
821            list.push(FilterValue::String(v.to_string()));
822        }
823        Self::In(field.into(), list)
824    }
825
826    /// Create a NOT filter.
827    #[inline]
828    #[allow(clippy::should_implement_trait)]
829    pub fn not(filter: Filter) -> Self {
830        if filter.is_none() {
831            return Self::None;
832        }
833        Self::Not(Box::new(filter))
834    }
835
836    /// Create an IN filter from a slice of values.
837    ///
838    /// This is more efficient than `Filter::In(field, values.into())` when you have a slice,
839    /// as it avoids intermediate collection.
840    ///
841    /// # Examples
842    ///
843    /// ```rust
844    /// use prax_query::filter::Filter;
845    ///
846    /// let ids: &[i64] = &[1, 2, 3, 4, 5];
847    /// let filter = Filter::in_slice("id", ids);
848    /// ```
849    #[inline]
850    pub fn in_slice<T: Into<FilterValue> + Clone>(
851        field: impl Into<FieldName>,
852        values: &[T],
853    ) -> Self {
854        let list: ValueList = values.iter().map(|v| v.clone().into()).collect();
855        Self::In(field.into(), list)
856    }
857
858    /// Create a NOT IN filter from a slice of values.
859    ///
860    /// # Examples
861    ///
862    /// ```rust
863    /// use prax_query::filter::Filter;
864    ///
865    /// let ids: &[i64] = &[1, 2, 3, 4, 5];
866    /// let filter = Filter::not_in_slice("id", ids);
867    /// ```
868    #[inline]
869    pub fn not_in_slice<T: Into<FilterValue> + Clone>(
870        field: impl Into<FieldName>,
871        values: &[T],
872    ) -> Self {
873        let list: ValueList = values.iter().map(|v| v.clone().into()).collect();
874        Self::NotIn(field.into(), list)
875    }
876
877    /// Create an IN filter from an array (const generic).
878    ///
879    /// This is useful when you know the size at compile time.
880    ///
881    /// # Examples
882    ///
883    /// ```rust
884    /// use prax_query::filter::Filter;
885    ///
886    /// let filter = Filter::in_array("status", ["active", "pending", "processing"]);
887    /// ```
888    #[inline]
889    pub fn in_array<T: Into<FilterValue>, const N: usize>(
890        field: impl Into<FieldName>,
891        values: [T; N],
892    ) -> Self {
893        let list: ValueList = values.into_iter().map(Into::into).collect();
894        Self::In(field.into(), list)
895    }
896
897    /// Create a NOT IN filter from an array (const generic).
898    #[inline]
899    pub fn not_in_array<T: Into<FilterValue>, const N: usize>(
900        field: impl Into<FieldName>,
901        values: [T; N],
902    ) -> Self {
903        let list: ValueList = values.into_iter().map(Into::into).collect();
904        Self::NotIn(field.into(), list)
905    }
906
907    /// Combine with another filter using AND.
908    pub fn and_then(self, other: Filter) -> Self {
909        if self.is_none() {
910            return other;
911        }
912        if other.is_none() {
913            return self;
914        }
915        match self {
916            Self::And(filters) => {
917                // Convert to Vec, add new filter, convert back to Box<[T]>
918                let mut vec: Vec<_> = filters.into_vec();
919                vec.push(other);
920                Self::And(vec.into_boxed_slice())
921            }
922            _ => Self::And(Box::new([self, other])),
923        }
924    }
925
926    /// Combine with another filter using OR.
927    pub fn or_else(self, other: Filter) -> Self {
928        if self.is_none() {
929            return other;
930        }
931        if other.is_none() {
932            return self;
933        }
934        match self {
935            Self::Or(filters) => {
936                // Convert to Vec, add new filter, convert back to Box<[T]>
937                let mut vec: Vec<_> = filters.into_vec();
938                vec.push(other);
939                Self::Or(vec.into_boxed_slice())
940            }
941            _ => Self::Or(Box::new([self, other])),
942        }
943    }
944
945    /// Generate SQL for this filter with parameter placeholders.
946    /// Returns (sql, params) where params are the values to bind.
947    pub fn to_sql(
948        &self,
949        param_offset: usize,
950        dialect: &dyn crate::dialect::SqlDialect,
951    ) -> (String, Vec<FilterValue>) {
952        let mut params = Vec::new();
953        let sql = self.to_sql_with_params(param_offset, &mut params, dialect);
954        (sql, params)
955    }
956
957    fn to_sql_with_params(
958        &self,
959        mut param_idx: usize,
960        params: &mut Vec<FilterValue>,
961        dialect: &dyn crate::dialect::SqlDialect,
962    ) -> String {
963        match self {
964            Self::None => "TRUE".to_string(),
965
966            Self::Equals(col, val) => {
967                let c = dialect.quote_ident(col);
968                if val.is_null() {
969                    format!("{} IS NULL", c)
970                } else {
971                    params.push(val.clone());
972                    param_idx += params.len();
973                    format!("{} = {}", c, dialect.placeholder(param_idx))
974                }
975            }
976            Self::NotEquals(col, val) => {
977                let c = dialect.quote_ident(col);
978                if val.is_null() {
979                    format!("{} IS NOT NULL", c)
980                } else {
981                    params.push(val.clone());
982                    param_idx += params.len();
983                    format!("{} != {}", c, dialect.placeholder(param_idx))
984                }
985            }
986
987            Self::Lt(col, val) => {
988                let c = dialect.quote_ident(col);
989                params.push(val.clone());
990                param_idx += params.len();
991                format!("{} < {}", c, dialect.placeholder(param_idx))
992            }
993            Self::Lte(col, val) => {
994                let c = dialect.quote_ident(col);
995                params.push(val.clone());
996                param_idx += params.len();
997                format!("{} <= {}", c, dialect.placeholder(param_idx))
998            }
999            Self::Gt(col, val) => {
1000                let c = dialect.quote_ident(col);
1001                params.push(val.clone());
1002                param_idx += params.len();
1003                format!("{} > {}", c, dialect.placeholder(param_idx))
1004            }
1005            Self::Gte(col, val) => {
1006                let c = dialect.quote_ident(col);
1007                params.push(val.clone());
1008                param_idx += params.len();
1009                format!("{} >= {}", c, dialect.placeholder(param_idx))
1010            }
1011
1012            Self::In(col, values) => {
1013                if values.is_empty() {
1014                    return "FALSE".to_string();
1015                }
1016                let c = dialect.quote_ident(col);
1017                let placeholders: Vec<_> = values
1018                    .iter()
1019                    .map(|v| {
1020                        params.push(v.clone());
1021                        param_idx += params.len();
1022                        dialect.placeholder(param_idx)
1023                    })
1024                    .collect();
1025                format!("{} IN ({})", c, placeholders.join(", "))
1026            }
1027            Self::NotIn(col, values) => {
1028                if values.is_empty() {
1029                    return "TRUE".to_string();
1030                }
1031                let c = dialect.quote_ident(col);
1032                let placeholders: Vec<_> = values
1033                    .iter()
1034                    .map(|v| {
1035                        params.push(v.clone());
1036                        param_idx += params.len();
1037                        dialect.placeholder(param_idx)
1038                    })
1039                    .collect();
1040                format!("{} NOT IN ({})", c, placeholders.join(", "))
1041            }
1042
1043            Self::Contains(col, val) => {
1044                let c = dialect.quote_ident(col);
1045                if let FilterValue::String(s) = val {
1046                    params.push(FilterValue::String(format!("%{}%", s)));
1047                } else {
1048                    params.push(val.clone());
1049                }
1050                param_idx += params.len();
1051                format!("{} LIKE {}", c, dialect.placeholder(param_idx))
1052            }
1053            Self::StartsWith(col, val) => {
1054                let c = dialect.quote_ident(col);
1055                if let FilterValue::String(s) = val {
1056                    params.push(FilterValue::String(format!("{}%", s)));
1057                } else {
1058                    params.push(val.clone());
1059                }
1060                param_idx += params.len();
1061                format!("{} LIKE {}", c, dialect.placeholder(param_idx))
1062            }
1063            Self::EndsWith(col, val) => {
1064                let c = dialect.quote_ident(col);
1065                if let FilterValue::String(s) = val {
1066                    params.push(FilterValue::String(format!("%{}", s)));
1067                } else {
1068                    params.push(val.clone());
1069                }
1070                param_idx += params.len();
1071                format!("{} LIKE {}", c, dialect.placeholder(param_idx))
1072            }
1073
1074            Self::IsNull(col) => {
1075                let c = dialect.quote_ident(col);
1076                format!("{} IS NULL", c)
1077            }
1078            Self::IsNotNull(col) => {
1079                let c = dialect.quote_ident(col);
1080                format!("{} IS NOT NULL", c)
1081            }
1082
1083            Self::And(filters) => {
1084                if filters.is_empty() {
1085                    return "TRUE".to_string();
1086                }
1087                let parts: Vec<_> = filters
1088                    .iter()
1089                    .map(|f| f.to_sql_with_params(param_idx + params.len(), params, dialect))
1090                    .collect();
1091                format!("({})", parts.join(" AND "))
1092            }
1093            Self::Or(filters) => {
1094                if filters.is_empty() {
1095                    return "FALSE".to_string();
1096                }
1097                let parts: Vec<_> = filters
1098                    .iter()
1099                    .map(|f| f.to_sql_with_params(param_idx + params.len(), params, dialect))
1100                    .collect();
1101                format!("({})", parts.join(" OR "))
1102            }
1103            Self::Not(filter) => {
1104                let inner = filter.to_sql_with_params(param_idx, params, dialect);
1105                format!("NOT ({})", inner)
1106            }
1107        }
1108    }
1109
1110    /// Create a builder for constructing AND filters with pre-allocated capacity.
1111    ///
1112    /// This is more efficient than using `Filter::and()` when you know the
1113    /// approximate number of conditions upfront.
1114    ///
1115    /// # Examples
1116    ///
1117    /// ```rust
1118    /// use prax_query::filter::{Filter, FilterValue};
1119    ///
1120    /// // Build an AND filter with pre-allocated capacity for 3 conditions
1121    /// let filter = Filter::and_builder(3)
1122    ///     .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
1123    ///     .push(Filter::Gt("score".into(), FilterValue::Int(100)))
1124    ///     .push(Filter::IsNotNull("email".into()))
1125    ///     .build();
1126    /// ```
1127    #[inline]
1128    pub fn and_builder(capacity: usize) -> AndFilterBuilder {
1129        AndFilterBuilder::with_capacity(capacity)
1130    }
1131
1132    /// Create a builder for constructing OR filters with pre-allocated capacity.
1133    ///
1134    /// This is more efficient than using `Filter::or()` when you know the
1135    /// approximate number of conditions upfront.
1136    ///
1137    /// # Examples
1138    ///
1139    /// ```rust
1140    /// use prax_query::filter::{Filter, FilterValue};
1141    ///
1142    /// // Build an OR filter with pre-allocated capacity for 2 conditions
1143    /// let filter = Filter::or_builder(2)
1144    ///     .push(Filter::Equals("role".into(), FilterValue::String("admin".into())))
1145    ///     .push(Filter::Equals("role".into(), FilterValue::String("moderator".into())))
1146    ///     .build();
1147    /// ```
1148    #[inline]
1149    pub fn or_builder(capacity: usize) -> OrFilterBuilder {
1150        OrFilterBuilder::with_capacity(capacity)
1151    }
1152
1153    /// Create a general-purpose filter builder.
1154    ///
1155    /// Use this for building complex filter trees with a fluent API.
1156    ///
1157    /// # Examples
1158    ///
1159    /// ```rust
1160    /// use prax_query::filter::Filter;
1161    ///
1162    /// let filter = Filter::builder()
1163    ///     .eq("status", "active")
1164    ///     .gt("age", 18)
1165    ///     .is_not_null("email")
1166    ///     .build_and();
1167    /// ```
1168    #[inline]
1169    pub fn builder() -> FluentFilterBuilder {
1170        FluentFilterBuilder::new()
1171    }
1172}
1173
1174/// Builder for constructing AND filters with pre-allocated capacity.
1175///
1176/// This avoids vector reallocations when the number of conditions is known upfront.
1177#[derive(Debug, Clone)]
1178pub struct AndFilterBuilder {
1179    filters: Vec<Filter>,
1180}
1181
1182impl AndFilterBuilder {
1183    /// Create a new builder with default capacity.
1184    #[inline]
1185    pub fn new() -> Self {
1186        Self {
1187            filters: Vec::new(),
1188        }
1189    }
1190
1191    /// Create a new builder with the specified capacity.
1192    #[inline]
1193    pub fn with_capacity(capacity: usize) -> Self {
1194        Self {
1195            filters: Vec::with_capacity(capacity),
1196        }
1197    }
1198
1199    /// Add a filter to the AND condition.
1200    #[inline]
1201    pub fn push(mut self, filter: Filter) -> Self {
1202        if !filter.is_none() {
1203            self.filters.push(filter);
1204        }
1205        self
1206    }
1207
1208    /// Add multiple filters to the AND condition.
1209    #[inline]
1210    pub fn extend(mut self, filters: impl IntoIterator<Item = Filter>) -> Self {
1211        self.filters
1212            .extend(filters.into_iter().filter(|f| !f.is_none()));
1213        self
1214    }
1215
1216    /// Add a filter conditionally.
1217    #[inline]
1218    pub fn push_if(self, condition: bool, filter: Filter) -> Self {
1219        if condition { self.push(filter) } else { self }
1220    }
1221
1222    /// Add a filter conditionally, evaluating the closure only if condition is true.
1223    #[inline]
1224    pub fn push_if_some<F>(self, opt: Option<F>) -> Self
1225    where
1226        F: Into<Filter>,
1227    {
1228        match opt {
1229            Some(f) => self.push(f.into()),
1230            None => self,
1231        }
1232    }
1233
1234    /// Build the final AND filter.
1235    #[inline]
1236    pub fn build(self) -> Filter {
1237        match self.filters.len() {
1238            0 => Filter::None,
1239            1 => self.filters.into_iter().next().unwrap(),
1240            _ => Filter::And(self.filters.into_boxed_slice()),
1241        }
1242    }
1243
1244    /// Get the current number of filters.
1245    #[inline]
1246    pub fn len(&self) -> usize {
1247        self.filters.len()
1248    }
1249
1250    /// Check if the builder is empty.
1251    #[inline]
1252    pub fn is_empty(&self) -> bool {
1253        self.filters.is_empty()
1254    }
1255}
1256
1257impl Default for AndFilterBuilder {
1258    fn default() -> Self {
1259        Self::new()
1260    }
1261}
1262
1263/// Builder for constructing OR filters with pre-allocated capacity.
1264///
1265/// This avoids vector reallocations when the number of conditions is known upfront.
1266#[derive(Debug, Clone)]
1267pub struct OrFilterBuilder {
1268    filters: Vec<Filter>,
1269}
1270
1271impl OrFilterBuilder {
1272    /// Create a new builder with default capacity.
1273    #[inline]
1274    pub fn new() -> Self {
1275        Self {
1276            filters: Vec::new(),
1277        }
1278    }
1279
1280    /// Create a new builder with the specified capacity.
1281    #[inline]
1282    pub fn with_capacity(capacity: usize) -> Self {
1283        Self {
1284            filters: Vec::with_capacity(capacity),
1285        }
1286    }
1287
1288    /// Add a filter to the OR condition.
1289    #[inline]
1290    pub fn push(mut self, filter: Filter) -> Self {
1291        if !filter.is_none() {
1292            self.filters.push(filter);
1293        }
1294        self
1295    }
1296
1297    /// Add multiple filters to the OR condition.
1298    #[inline]
1299    pub fn extend(mut self, filters: impl IntoIterator<Item = Filter>) -> Self {
1300        self.filters
1301            .extend(filters.into_iter().filter(|f| !f.is_none()));
1302        self
1303    }
1304
1305    /// Add a filter conditionally.
1306    #[inline]
1307    pub fn push_if(self, condition: bool, filter: Filter) -> Self {
1308        if condition { self.push(filter) } else { self }
1309    }
1310
1311    /// Add a filter conditionally, evaluating the closure only if condition is true.
1312    #[inline]
1313    pub fn push_if_some<F>(self, opt: Option<F>) -> Self
1314    where
1315        F: Into<Filter>,
1316    {
1317        match opt {
1318            Some(f) => self.push(f.into()),
1319            None => self,
1320        }
1321    }
1322
1323    /// Build the final OR filter.
1324    #[inline]
1325    pub fn build(self) -> Filter {
1326        match self.filters.len() {
1327            0 => Filter::None,
1328            1 => self.filters.into_iter().next().unwrap(),
1329            _ => Filter::Or(self.filters.into_boxed_slice()),
1330        }
1331    }
1332
1333    /// Get the current number of filters.
1334    #[inline]
1335    pub fn len(&self) -> usize {
1336        self.filters.len()
1337    }
1338
1339    /// Check if the builder is empty.
1340    #[inline]
1341    pub fn is_empty(&self) -> bool {
1342        self.filters.is_empty()
1343    }
1344}
1345
1346impl Default for OrFilterBuilder {
1347    fn default() -> Self {
1348        Self::new()
1349    }
1350}
1351
1352/// A fluent builder for constructing filters with a convenient API.
1353///
1354/// This builder collects conditions and can produce either an AND or OR filter.
1355///
1356/// # Examples
1357///
1358/// ```rust
1359/// use prax_query::filter::Filter;
1360///
1361/// // Build an AND filter
1362/// let filter = Filter::builder()
1363///     .eq("active", true)
1364///     .gt("score", 100)
1365///     .contains("email", "@example.com")
1366///     .build_and();
1367///
1368/// // Build an OR filter with capacity hint
1369/// let filter = Filter::builder()
1370///     .with_capacity(3)
1371///     .eq("role", "admin")
1372///     .eq("role", "moderator")
1373///     .eq("role", "owner")
1374///     .build_or();
1375/// ```
1376#[derive(Debug, Clone)]
1377pub struct FluentFilterBuilder {
1378    filters: Vec<Filter>,
1379}
1380
1381impl FluentFilterBuilder {
1382    /// Create a new fluent builder.
1383    #[inline]
1384    pub fn new() -> Self {
1385        Self {
1386            filters: Vec::new(),
1387        }
1388    }
1389
1390    /// Set the capacity hint for the internal vector.
1391    #[inline]
1392    pub fn with_capacity(mut self, capacity: usize) -> Self {
1393        self.filters.reserve(capacity);
1394        self
1395    }
1396
1397    /// Add an equals filter.
1398    #[inline]
1399    pub fn eq<F, V>(mut self, field: F, value: V) -> Self
1400    where
1401        F: Into<FieldName>,
1402        V: Into<FilterValue>,
1403    {
1404        self.filters
1405            .push(Filter::Equals(field.into(), value.into()));
1406        self
1407    }
1408
1409    /// Add a not equals filter.
1410    #[inline]
1411    pub fn ne<F, V>(mut self, field: F, value: V) -> Self
1412    where
1413        F: Into<FieldName>,
1414        V: Into<FilterValue>,
1415    {
1416        self.filters
1417            .push(Filter::NotEquals(field.into(), value.into()));
1418        self
1419    }
1420
1421    /// Add a less than filter.
1422    #[inline]
1423    pub fn lt<F, V>(mut self, field: F, value: V) -> Self
1424    where
1425        F: Into<FieldName>,
1426        V: Into<FilterValue>,
1427    {
1428        self.filters.push(Filter::Lt(field.into(), value.into()));
1429        self
1430    }
1431
1432    /// Add a less than or equal filter.
1433    #[inline]
1434    pub fn lte<F, V>(mut self, field: F, value: V) -> Self
1435    where
1436        F: Into<FieldName>,
1437        V: Into<FilterValue>,
1438    {
1439        self.filters.push(Filter::Lte(field.into(), value.into()));
1440        self
1441    }
1442
1443    /// Add a greater than filter.
1444    #[inline]
1445    pub fn gt<F, V>(mut self, field: F, value: V) -> Self
1446    where
1447        F: Into<FieldName>,
1448        V: Into<FilterValue>,
1449    {
1450        self.filters.push(Filter::Gt(field.into(), value.into()));
1451        self
1452    }
1453
1454    /// Add a greater than or equal filter.
1455    #[inline]
1456    pub fn gte<F, V>(mut self, field: F, value: V) -> Self
1457    where
1458        F: Into<FieldName>,
1459        V: Into<FilterValue>,
1460    {
1461        self.filters.push(Filter::Gte(field.into(), value.into()));
1462        self
1463    }
1464
1465    /// Add an IN filter.
1466    #[inline]
1467    pub fn is_in<F, I, V>(mut self, field: F, values: I) -> Self
1468    where
1469        F: Into<FieldName>,
1470        I: IntoIterator<Item = V>,
1471        V: Into<FilterValue>,
1472    {
1473        self.filters.push(Filter::In(
1474            field.into(),
1475            values.into_iter().map(Into::into).collect(),
1476        ));
1477        self
1478    }
1479
1480    /// Add a NOT IN filter.
1481    #[inline]
1482    pub fn not_in<F, I, V>(mut self, field: F, values: I) -> Self
1483    where
1484        F: Into<FieldName>,
1485        I: IntoIterator<Item = V>,
1486        V: Into<FilterValue>,
1487    {
1488        self.filters.push(Filter::NotIn(
1489            field.into(),
1490            values.into_iter().map(Into::into).collect(),
1491        ));
1492        self
1493    }
1494
1495    /// Add a contains filter (LIKE %value%).
1496    #[inline]
1497    pub fn contains<F, V>(mut self, field: F, value: V) -> Self
1498    where
1499        F: Into<FieldName>,
1500        V: Into<FilterValue>,
1501    {
1502        self.filters
1503            .push(Filter::Contains(field.into(), value.into()));
1504        self
1505    }
1506
1507    /// Add a starts with filter (LIKE value%).
1508    #[inline]
1509    pub fn starts_with<F, V>(mut self, field: F, value: V) -> Self
1510    where
1511        F: Into<FieldName>,
1512        V: Into<FilterValue>,
1513    {
1514        self.filters
1515            .push(Filter::StartsWith(field.into(), value.into()));
1516        self
1517    }
1518
1519    /// Add an ends with filter (LIKE %value).
1520    #[inline]
1521    pub fn ends_with<F, V>(mut self, field: F, value: V) -> Self
1522    where
1523        F: Into<FieldName>,
1524        V: Into<FilterValue>,
1525    {
1526        self.filters
1527            .push(Filter::EndsWith(field.into(), value.into()));
1528        self
1529    }
1530
1531    /// Add an IS NULL filter.
1532    #[inline]
1533    pub fn is_null<F>(mut self, field: F) -> Self
1534    where
1535        F: Into<FieldName>,
1536    {
1537        self.filters.push(Filter::IsNull(field.into()));
1538        self
1539    }
1540
1541    /// Add an IS NOT NULL filter.
1542    #[inline]
1543    pub fn is_not_null<F>(mut self, field: F) -> Self
1544    where
1545        F: Into<FieldName>,
1546    {
1547        self.filters.push(Filter::IsNotNull(field.into()));
1548        self
1549    }
1550
1551    /// Add a raw filter directly.
1552    #[inline]
1553    pub fn filter(mut self, filter: Filter) -> Self {
1554        if !filter.is_none() {
1555            self.filters.push(filter);
1556        }
1557        self
1558    }
1559
1560    /// Add a filter conditionally.
1561    #[inline]
1562    pub fn filter_if(self, condition: bool, filter: Filter) -> Self {
1563        if condition { self.filter(filter) } else { self }
1564    }
1565
1566    /// Add a filter conditionally if the option is Some.
1567    #[inline]
1568    pub fn filter_if_some<F>(self, opt: Option<F>) -> Self
1569    where
1570        F: Into<Filter>,
1571    {
1572        match opt {
1573            Some(f) => self.filter(f.into()),
1574            None => self,
1575        }
1576    }
1577
1578    /// Build an AND filter from all collected conditions.
1579    #[inline]
1580    pub fn build_and(self) -> Filter {
1581        let filters: Vec<_> = self.filters.into_iter().filter(|f| !f.is_none()).collect();
1582        match filters.len() {
1583            0 => Filter::None,
1584            1 => filters.into_iter().next().unwrap(),
1585            _ => Filter::And(filters.into_boxed_slice()),
1586        }
1587    }
1588
1589    /// Build an OR filter from all collected conditions.
1590    #[inline]
1591    pub fn build_or(self) -> Filter {
1592        let filters: Vec<_> = self.filters.into_iter().filter(|f| !f.is_none()).collect();
1593        match filters.len() {
1594            0 => Filter::None,
1595            1 => filters.into_iter().next().unwrap(),
1596            _ => Filter::Or(filters.into_boxed_slice()),
1597        }
1598    }
1599
1600    /// Get the current number of filters.
1601    #[inline]
1602    pub fn len(&self) -> usize {
1603        self.filters.len()
1604    }
1605
1606    /// Check if the builder is empty.
1607    #[inline]
1608    pub fn is_empty(&self) -> bool {
1609        self.filters.is_empty()
1610    }
1611}
1612
1613impl Default for FluentFilterBuilder {
1614    fn default() -> Self {
1615        Self::new()
1616    }
1617}
1618
1619#[cfg(test)]
1620mod tests {
1621    use super::*;
1622
1623    #[test]
1624    fn test_filter_value_from() {
1625        assert_eq!(FilterValue::from(42i32), FilterValue::Int(42));
1626        assert_eq!(
1627            FilterValue::from("hello"),
1628            FilterValue::String("hello".to_string())
1629        );
1630        assert_eq!(FilterValue::from(true), FilterValue::Bool(true));
1631    }
1632
1633    #[test]
1634    fn test_scalar_filter_equals() {
1635        let filter = ScalarFilter::Equals("test@example.com".to_string()).into_filter("email");
1636
1637        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1638        assert_eq!(sql, r#""email" = $1"#);
1639        assert_eq!(params.len(), 1);
1640    }
1641
1642    #[test]
1643    fn test_filter_and() {
1644        let f1 = Filter::Equals("name".into(), "Alice".into());
1645        let f2 = Filter::Gt("age".into(), FilterValue::Int(18));
1646        let combined = Filter::and([f1, f2]);
1647
1648        let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
1649        assert!(sql.contains("AND"));
1650        assert_eq!(params.len(), 2);
1651    }
1652
1653    #[test]
1654    fn test_filter_or() {
1655        let f1 = Filter::Equals("status".into(), "active".into());
1656        let f2 = Filter::Equals("status".into(), "pending".into());
1657        let combined = Filter::or([f1, f2]);
1658
1659        let (sql, _) = combined.to_sql(0, &crate::dialect::Postgres);
1660        assert!(sql.contains("OR"));
1661    }
1662
1663    #[test]
1664    fn test_filter_not() {
1665        let filter = Filter::not(Filter::Equals("deleted".into(), FilterValue::Bool(true)));
1666
1667        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
1668        assert!(sql.contains("NOT"));
1669    }
1670
1671    #[test]
1672    fn test_filter_is_null() {
1673        let filter = Filter::IsNull("deleted_at".into());
1674        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1675        assert_eq!(sql, r#""deleted_at" IS NULL"#);
1676        assert!(params.is_empty());
1677    }
1678
1679    #[test]
1680    fn test_filter_in() {
1681        let filter = Filter::In("status".into(), vec!["active".into(), "pending".into()]);
1682        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1683        assert!(sql.contains("IN"));
1684        assert_eq!(params.len(), 2);
1685    }
1686
1687    #[test]
1688    fn test_filter_contains() {
1689        let filter = Filter::Contains("email".into(), "example".into());
1690        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1691        assert!(sql.contains("LIKE"));
1692        assert_eq!(params.len(), 1);
1693        if let FilterValue::String(s) = &params[0] {
1694            assert!(s.contains("%example%"));
1695        }
1696    }
1697
1698    // ==================== FilterValue Tests ====================
1699
1700    #[test]
1701    fn test_filter_value_is_null() {
1702        assert!(FilterValue::Null.is_null());
1703        assert!(!FilterValue::Bool(false).is_null());
1704        assert!(!FilterValue::Int(0).is_null());
1705        assert!(!FilterValue::Float(0.0).is_null());
1706        assert!(!FilterValue::String("".to_string()).is_null());
1707    }
1708
1709    #[test]
1710    fn test_filter_value_from_i64() {
1711        assert_eq!(FilterValue::from(42i64), FilterValue::Int(42));
1712        assert_eq!(FilterValue::from(-100i64), FilterValue::Int(-100));
1713    }
1714
1715    #[test]
1716    fn test_filter_value_from_f64() {
1717        assert_eq!(FilterValue::from(3.14f64), FilterValue::Float(3.14));
1718    }
1719
1720    #[test]
1721    fn test_filter_value_from_string() {
1722        assert_eq!(
1723            FilterValue::from("hello".to_string()),
1724            FilterValue::String("hello".to_string())
1725        );
1726    }
1727
1728    #[test]
1729    fn test_filter_value_from_vec() {
1730        let values: Vec<i32> = vec![1, 2, 3];
1731        let filter_val: FilterValue = values.into();
1732        if let FilterValue::List(list) = filter_val {
1733            assert_eq!(list.len(), 3);
1734            assert_eq!(list[0], FilterValue::Int(1));
1735            assert_eq!(list[1], FilterValue::Int(2));
1736            assert_eq!(list[2], FilterValue::Int(3));
1737        } else {
1738            panic!("Expected List");
1739        }
1740    }
1741
1742    #[test]
1743    fn test_filter_value_from_option_some() {
1744        let val: FilterValue = Some(42i32).into();
1745        assert_eq!(val, FilterValue::Int(42));
1746    }
1747
1748    #[test]
1749    fn test_filter_value_from_option_none() {
1750        let val: FilterValue = Option::<i32>::None.into();
1751        assert_eq!(val, FilterValue::Null);
1752    }
1753
1754    // ==================== ScalarFilter Tests ====================
1755
1756    #[test]
1757    fn test_scalar_filter_not() {
1758        let filter = ScalarFilter::Not(Box::new("test".to_string())).into_filter("name");
1759        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1760        assert_eq!(sql, r#""name" != $1"#);
1761        assert_eq!(params.len(), 1);
1762    }
1763
1764    #[test]
1765    fn test_scalar_filter_in() {
1766        let filter = ScalarFilter::In(vec!["a".to_string(), "b".to_string()]).into_filter("status");
1767        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1768        assert!(sql.contains("IN"));
1769        assert_eq!(params.len(), 2);
1770    }
1771
1772    #[test]
1773    fn test_scalar_filter_not_in() {
1774        let filter = ScalarFilter::NotIn(vec!["x".to_string()]).into_filter("status");
1775        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1776        assert!(sql.contains("NOT IN"));
1777        assert_eq!(params.len(), 1);
1778    }
1779
1780    #[test]
1781    fn test_scalar_filter_lt() {
1782        let filter = ScalarFilter::Lt(100i32).into_filter("price");
1783        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1784        assert_eq!(sql, r#""price" < $1"#);
1785        assert_eq!(params.len(), 1);
1786    }
1787
1788    #[test]
1789    fn test_scalar_filter_lte() {
1790        let filter = ScalarFilter::Lte(100i32).into_filter("price");
1791        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1792        assert_eq!(sql, r#""price" <= $1"#);
1793        assert_eq!(params.len(), 1);
1794    }
1795
1796    #[test]
1797    fn test_scalar_filter_gt() {
1798        let filter = ScalarFilter::Gt(0i32).into_filter("quantity");
1799        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1800        assert_eq!(sql, r#""quantity" > $1"#);
1801        assert_eq!(params.len(), 1);
1802    }
1803
1804    #[test]
1805    fn test_scalar_filter_gte() {
1806        let filter = ScalarFilter::Gte(0i32).into_filter("quantity");
1807        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1808        assert_eq!(sql, r#""quantity" >= $1"#);
1809        assert_eq!(params.len(), 1);
1810    }
1811
1812    #[test]
1813    fn test_scalar_filter_starts_with() {
1814        let filter = ScalarFilter::StartsWith("prefix".to_string()).into_filter("name");
1815        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1816        assert!(sql.contains("LIKE"));
1817        assert_eq!(params.len(), 1);
1818        if let FilterValue::String(s) = &params[0] {
1819            assert!(s.starts_with("prefix"));
1820            assert!(s.ends_with("%"));
1821        }
1822    }
1823
1824    #[test]
1825    fn test_scalar_filter_ends_with() {
1826        let filter = ScalarFilter::EndsWith("suffix".to_string()).into_filter("name");
1827        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1828        assert!(sql.contains("LIKE"));
1829        assert_eq!(params.len(), 1);
1830        if let FilterValue::String(s) = &params[0] {
1831            assert!(s.starts_with("%"));
1832            assert!(s.ends_with("suffix"));
1833        }
1834    }
1835
1836    #[test]
1837    fn test_scalar_filter_is_null() {
1838        let filter = ScalarFilter::<String>::IsNull.into_filter("deleted_at");
1839        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1840        assert_eq!(sql, r#""deleted_at" IS NULL"#);
1841        assert!(params.is_empty());
1842    }
1843
1844    #[test]
1845    fn test_scalar_filter_is_not_null() {
1846        let filter = ScalarFilter::<String>::IsNotNull.into_filter("name");
1847        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1848        assert_eq!(sql, r#""name" IS NOT NULL"#);
1849        assert!(params.is_empty());
1850    }
1851
1852    // ==================== Filter Tests ====================
1853
1854    #[test]
1855    fn test_filter_none() {
1856        let filter = Filter::none();
1857        assert!(filter.is_none());
1858        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1859        assert_eq!(sql, "TRUE"); // Filter::None generates TRUE
1860        assert!(params.is_empty());
1861    }
1862
1863    #[test]
1864    fn test_filter_not_equals() {
1865        let filter = Filter::NotEquals("status".into(), "deleted".into());
1866        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1867        assert_eq!(sql, r#""status" != $1"#);
1868        assert_eq!(params.len(), 1);
1869    }
1870
1871    #[test]
1872    fn test_filter_lte() {
1873        let filter = Filter::Lte("price".into(), FilterValue::Int(100));
1874        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1875        assert_eq!(sql, r#""price" <= $1"#);
1876        assert_eq!(params.len(), 1);
1877    }
1878
1879    #[test]
1880    fn test_filter_gte() {
1881        let filter = Filter::Gte("quantity".into(), FilterValue::Int(0));
1882        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1883        assert_eq!(sql, r#""quantity" >= $1"#);
1884        assert_eq!(params.len(), 1);
1885    }
1886
1887    #[test]
1888    fn test_filter_not_in() {
1889        let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
1890        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1891        assert!(sql.contains("NOT IN"));
1892        assert_eq!(params.len(), 2);
1893    }
1894
1895    #[test]
1896    fn test_filter_starts_with() {
1897        let filter = Filter::StartsWith("email".into(), "admin".into());
1898        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1899        assert!(sql.contains("LIKE"));
1900        assert_eq!(params.len(), 1);
1901    }
1902
1903    #[test]
1904    fn test_filter_ends_with() {
1905        let filter = Filter::EndsWith("email".into(), "@example.com".into());
1906        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1907        assert!(sql.contains("LIKE"));
1908        assert_eq!(params.len(), 1);
1909    }
1910
1911    #[test]
1912    fn test_filter_is_not_null() {
1913        let filter = Filter::IsNotNull("name".into());
1914        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1915        assert_eq!(sql, r#""name" IS NOT NULL"#);
1916        assert!(params.is_empty());
1917    }
1918
1919    // ==================== Filter Combination Tests ====================
1920
1921    #[test]
1922    fn test_filter_and_empty() {
1923        let filter = Filter::and([]);
1924        assert!(filter.is_none());
1925    }
1926
1927    #[test]
1928    fn test_filter_and_single() {
1929        let f = Filter::Equals("name".into(), "Alice".into());
1930        let combined = Filter::and([f.clone()]);
1931        assert_eq!(combined, f);
1932    }
1933
1934    #[test]
1935    fn test_filter_and_with_none() {
1936        let f1 = Filter::Equals("name".into(), "Alice".into());
1937        let f2 = Filter::None;
1938        let combined = Filter::and([f1.clone(), f2]);
1939        assert_eq!(combined, f1);
1940    }
1941
1942    #[test]
1943    fn test_filter_or_empty() {
1944        let filter = Filter::or([]);
1945        assert!(filter.is_none());
1946    }
1947
1948    #[test]
1949    fn test_filter_or_single() {
1950        let f = Filter::Equals("status".into(), "active".into());
1951        let combined = Filter::or([f.clone()]);
1952        assert_eq!(combined, f);
1953    }
1954
1955    #[test]
1956    fn test_filter_or_with_none() {
1957        let f1 = Filter::Equals("status".into(), "active".into());
1958        let f2 = Filter::None;
1959        let combined = Filter::or([f1.clone(), f2]);
1960        assert_eq!(combined, f1);
1961    }
1962
1963    #[test]
1964    fn test_filter_not_none() {
1965        let filter = Filter::not(Filter::None);
1966        assert!(filter.is_none());
1967    }
1968
1969    #[test]
1970    fn test_filter_and_then() {
1971        let f1 = Filter::Equals("name".into(), "Alice".into());
1972        let f2 = Filter::Gt("age".into(), FilterValue::Int(18));
1973        let combined = f1.and_then(f2);
1974
1975        let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
1976        assert!(sql.contains("AND"));
1977        assert_eq!(params.len(), 2);
1978    }
1979
1980    #[test]
1981    fn test_filter_and_then_with_none_first() {
1982        let f1 = Filter::None;
1983        let f2 = Filter::Equals("name".into(), "Bob".into());
1984        let combined = f1.and_then(f2.clone());
1985        assert_eq!(combined, f2);
1986    }
1987
1988    #[test]
1989    fn test_filter_and_then_with_none_second() {
1990        let f1 = Filter::Equals("name".into(), "Alice".into());
1991        let f2 = Filter::None;
1992        let combined = f1.clone().and_then(f2);
1993        assert_eq!(combined, f1);
1994    }
1995
1996    #[test]
1997    fn test_filter_and_then_chained() {
1998        let f1 = Filter::Equals("a".into(), "1".into());
1999        let f2 = Filter::Equals("b".into(), "2".into());
2000        let f3 = Filter::Equals("c".into(), "3".into());
2001        let combined = f1.and_then(f2).and_then(f3);
2002
2003        let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
2004        assert!(sql.contains("AND"));
2005        assert_eq!(params.len(), 3);
2006    }
2007
2008    #[test]
2009    fn test_filter_or_else() {
2010        let f1 = Filter::Equals("status".into(), "active".into());
2011        let f2 = Filter::Equals("status".into(), "pending".into());
2012        let combined = f1.or_else(f2);
2013
2014        let (sql, _) = combined.to_sql(0, &crate::dialect::Postgres);
2015        assert!(sql.contains("OR"));
2016    }
2017
2018    #[test]
2019    fn test_filter_or_else_with_none_first() {
2020        let f1 = Filter::None;
2021        let f2 = Filter::Equals("name".into(), "Bob".into());
2022        let combined = f1.or_else(f2.clone());
2023        assert_eq!(combined, f2);
2024    }
2025
2026    #[test]
2027    fn test_filter_or_else_with_none_second() {
2028        let f1 = Filter::Equals("name".into(), "Alice".into());
2029        let f2 = Filter::None;
2030        let combined = f1.clone().or_else(f2);
2031        assert_eq!(combined, f1);
2032    }
2033
2034    // ==================== Complex Filter SQL Generation ====================
2035
2036    #[test]
2037    fn test_filter_nested_and_or() {
2038        let f1 = Filter::Equals("status".into(), "active".into());
2039        let f2 = Filter::and([
2040            Filter::Gt("age".into(), FilterValue::Int(18)),
2041            Filter::Lt("age".into(), FilterValue::Int(65)),
2042        ]);
2043        let combined = Filter::and([f1, f2]);
2044
2045        let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
2046        assert!(sql.contains("AND"));
2047        assert_eq!(params.len(), 3);
2048    }
2049
2050    #[test]
2051    fn test_filter_nested_not() {
2052        let inner = Filter::and([
2053            Filter::Equals("status".into(), "deleted".into()),
2054            Filter::Equals("archived".into(), FilterValue::Bool(true)),
2055        ]);
2056        let filter = Filter::not(inner);
2057
2058        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2059        assert!(sql.contains("NOT"));
2060        assert!(sql.contains("AND"));
2061        assert_eq!(params.len(), 2);
2062    }
2063
2064    #[test]
2065    fn test_filter_with_json_value() {
2066        let json_val = serde_json::json!({"key": "value"});
2067        let filter = Filter::Equals("metadata".into(), FilterValue::Json(json_val));
2068        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2069        assert_eq!(sql, r#""metadata" = $1"#);
2070        assert_eq!(params.len(), 1);
2071    }
2072
2073    #[test]
2074    fn test_filter_in_empty_list() {
2075        let filter = Filter::In("status".into(), vec![]);
2076        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2077        // Empty IN generates FALSE (no match possible)
2078        assert!(
2079            sql.contains("FALSE")
2080                || sql.contains("1=0")
2081                || sql.is_empty()
2082                || sql.contains("status")
2083        );
2084        assert!(params.is_empty());
2085    }
2086
2087    #[test]
2088    fn test_filter_with_null_value() {
2089        // When filtering with Null value, it uses IS NULL instead of = $1
2090        let filter = Filter::IsNull("deleted_at".into());
2091        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2092        assert!(sql.contains("deleted_at"));
2093        assert!(sql.contains("IS NULL"));
2094        assert!(params.is_empty());
2095    }
2096
2097    // ==================== Builder Tests ====================
2098
2099    #[test]
2100    fn test_and_builder_basic() {
2101        let filter = Filter::and_builder(3)
2102            .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2103            .push(Filter::Gt("score".into(), FilterValue::Int(100)))
2104            .push(Filter::IsNotNull("email".into()))
2105            .build();
2106
2107        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2108        assert!(sql.contains("AND"));
2109        assert_eq!(params.len(), 2); // score and active, IS NOT NULL has no param
2110    }
2111
2112    #[test]
2113    fn test_and_builder_empty() {
2114        let filter = Filter::and_builder(0).build();
2115        assert!(filter.is_none());
2116    }
2117
2118    #[test]
2119    fn test_and_builder_single() {
2120        let filter = Filter::and_builder(1)
2121            .push(Filter::Equals("id".into(), FilterValue::Int(42)))
2122            .build();
2123
2124        // Single filter should not be wrapped in AND
2125        assert!(matches!(filter, Filter::Equals(_, _)));
2126    }
2127
2128    #[test]
2129    fn test_and_builder_filters_none() {
2130        let filter = Filter::and_builder(3)
2131            .push(Filter::None)
2132            .push(Filter::Equals("id".into(), FilterValue::Int(1)))
2133            .push(Filter::None)
2134            .build();
2135
2136        // None filters should be filtered out, leaving single filter
2137        assert!(matches!(filter, Filter::Equals(_, _)));
2138    }
2139
2140    #[test]
2141    fn test_and_builder_push_if() {
2142        let include_deleted = false;
2143        let filter = Filter::and_builder(2)
2144            .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2145            .push_if(include_deleted, Filter::IsNull("deleted_at".into()))
2146            .build();
2147
2148        // Should only have active filter since include_deleted is false
2149        assert!(matches!(filter, Filter::Equals(_, _)));
2150    }
2151
2152    #[test]
2153    fn test_or_builder_basic() {
2154        let filter = Filter::or_builder(2)
2155            .push(Filter::Equals(
2156                "role".into(),
2157                FilterValue::String("admin".into()),
2158            ))
2159            .push(Filter::Equals(
2160                "role".into(),
2161                FilterValue::String("moderator".into()),
2162            ))
2163            .build();
2164
2165        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2166        assert!(sql.contains("OR"));
2167    }
2168
2169    #[test]
2170    fn test_or_builder_empty() {
2171        let filter = Filter::or_builder(0).build();
2172        assert!(filter.is_none());
2173    }
2174
2175    #[test]
2176    fn test_or_builder_single() {
2177        let filter = Filter::or_builder(1)
2178            .push(Filter::Equals("id".into(), FilterValue::Int(42)))
2179            .build();
2180
2181        // Single filter should not be wrapped in OR
2182        assert!(matches!(filter, Filter::Equals(_, _)));
2183    }
2184
2185    #[test]
2186    fn test_fluent_builder_and() {
2187        let filter = Filter::builder()
2188            .eq("status", "active")
2189            .gt("age", 18)
2190            .is_not_null("email")
2191            .build_and();
2192
2193        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2194        assert!(sql.contains("AND"));
2195        assert_eq!(params.len(), 2);
2196    }
2197
2198    #[test]
2199    fn test_fluent_builder_or() {
2200        let filter = Filter::builder()
2201            .eq("role", "admin")
2202            .eq("role", "moderator")
2203            .build_or();
2204
2205        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2206        assert!(sql.contains("OR"));
2207    }
2208
2209    #[test]
2210    fn test_fluent_builder_with_capacity() {
2211        let filter = Filter::builder()
2212            .with_capacity(5)
2213            .eq("a", 1)
2214            .ne("b", 2)
2215            .lt("c", 3)
2216            .lte("d", 4)
2217            .gte("e", 5)
2218            .build_and();
2219
2220        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2221        assert!(sql.contains("AND"));
2222        assert_eq!(params.len(), 5);
2223    }
2224
2225    #[test]
2226    fn test_fluent_builder_string_operations() {
2227        let filter = Filter::builder()
2228            .contains("name", "john")
2229            .starts_with("email", "admin")
2230            .ends_with("domain", ".com")
2231            .build_and();
2232
2233        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2234        assert!(sql.contains("LIKE"));
2235    }
2236
2237    #[test]
2238    fn test_fluent_builder_null_operations() {
2239        let filter = Filter::builder()
2240            .is_null("deleted_at")
2241            .is_not_null("created_at")
2242            .build_and();
2243
2244        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2245        assert!(sql.contains("IS NULL"));
2246        assert!(sql.contains("IS NOT NULL"));
2247    }
2248
2249    #[test]
2250    fn test_fluent_builder_in_operations() {
2251        let filter = Filter::builder()
2252            .is_in("status", vec!["pending", "processing"])
2253            .not_in("role", vec!["banned", "suspended"])
2254            .build_and();
2255
2256        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2257        assert!(sql.contains("IN"));
2258        assert!(sql.contains("NOT IN"));
2259    }
2260
2261    #[test]
2262    fn test_fluent_builder_filter_if() {
2263        let include_archived = false;
2264        let filter = Filter::builder()
2265            .eq("active", true)
2266            .filter_if(
2267                include_archived,
2268                Filter::Equals("archived".into(), FilterValue::Bool(true)),
2269            )
2270            .build_and();
2271
2272        // Should only have active filter
2273        assert!(matches!(filter, Filter::Equals(_, _)));
2274    }
2275
2276    #[test]
2277    fn test_fluent_builder_filter_if_some() {
2278        let maybe_status: Option<Filter> = Some(Filter::Equals("status".into(), "active".into()));
2279        let filter = Filter::builder()
2280            .eq("id", 1)
2281            .filter_if_some(maybe_status)
2282            .build_and();
2283
2284        assert!(matches!(filter, Filter::And(_)));
2285    }
2286
2287    #[test]
2288    fn test_and_builder_extend() {
2289        let extra_filters = vec![
2290            Filter::Gt("score".into(), FilterValue::Int(100)),
2291            Filter::Lt("score".into(), FilterValue::Int(1000)),
2292        ];
2293
2294        let filter = Filter::and_builder(3)
2295            .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2296            .extend(extra_filters)
2297            .build();
2298
2299        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2300        assert!(sql.contains("AND"));
2301        assert_eq!(params.len(), 3);
2302    }
2303
2304    #[test]
2305    fn test_builder_len_and_is_empty() {
2306        let mut builder = AndFilterBuilder::new();
2307        assert!(builder.is_empty());
2308        assert_eq!(builder.len(), 0);
2309
2310        builder = builder.push(Filter::Equals("id".into(), FilterValue::Int(1)));
2311        assert!(!builder.is_empty());
2312        assert_eq!(builder.len(), 1);
2313    }
2314
2315    // ==================== and2/or2 Tests ====================
2316
2317    #[test]
2318    fn test_and2_both_valid() {
2319        let a = Filter::Equals("id".into(), FilterValue::Int(1));
2320        let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2321        let filter = Filter::and2(a, b);
2322
2323        assert!(matches!(filter, Filter::And(_)));
2324        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2325        assert!(sql.contains("AND"));
2326        assert_eq!(params.len(), 2);
2327    }
2328
2329    #[test]
2330    fn test_and2_first_none() {
2331        let a = Filter::None;
2332        let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2333        let filter = Filter::and2(a, b.clone());
2334
2335        assert_eq!(filter, b);
2336    }
2337
2338    #[test]
2339    fn test_and2_second_none() {
2340        let a = Filter::Equals("id".into(), FilterValue::Int(1));
2341        let b = Filter::None;
2342        let filter = Filter::and2(a.clone(), b);
2343
2344        assert_eq!(filter, a);
2345    }
2346
2347    #[test]
2348    fn test_and2_both_none() {
2349        let filter = Filter::and2(Filter::None, Filter::None);
2350        assert!(filter.is_none());
2351    }
2352
2353    #[test]
2354    fn test_or2_both_valid() {
2355        let a = Filter::Equals("role".into(), FilterValue::String("admin".into()));
2356        let b = Filter::Equals("role".into(), FilterValue::String("mod".into()));
2357        let filter = Filter::or2(a, b);
2358
2359        assert!(matches!(filter, Filter::Or(_)));
2360        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2361        assert!(sql.contains("OR"));
2362    }
2363
2364    #[test]
2365    fn test_or2_first_none() {
2366        let a = Filter::None;
2367        let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2368        let filter = Filter::or2(a, b.clone());
2369
2370        assert_eq!(filter, b);
2371    }
2372
2373    #[test]
2374    fn test_or2_second_none() {
2375        let a = Filter::Equals("id".into(), FilterValue::Int(1));
2376        let b = Filter::None;
2377        let filter = Filter::or2(a.clone(), b);
2378
2379        assert_eq!(filter, a);
2380    }
2381
2382    #[test]
2383    fn test_or2_both_none() {
2384        let filter = Filter::or2(Filter::None, Filter::None);
2385        assert!(filter.is_none());
2386    }
2387
2388    // ==================== SQL Injection Prevention Tests ====================
2389
2390    #[test]
2391    fn to_sql_quotes_column_names_against_injection() {
2392        use crate::dialect::{Mssql, Mysql, Postgres};
2393
2394        // Malicious column name attempts to break out of the identifier.
2395        let filter = Filter::Equals(r#"id" OR 1=1--"#.into(), FilterValue::Int(1));
2396
2397        let (sql_pg, _) = filter.to_sql(0, &Postgres);
2398        assert!(
2399            sql_pg.starts_with(r#""id"" OR 1=1--" ="#),
2400            "postgres did not quote col; got: {sql_pg}"
2401        );
2402
2403        let (sql_my, _) = filter.to_sql(0, &Mysql);
2404        assert!(
2405            sql_my.starts_with(r#"`id" OR 1=1--` ="#),
2406            "mysql did not quote col; got: {sql_my}"
2407        );
2408
2409        let (sql_ms, _) = filter.to_sql(0, &Mssql);
2410        assert!(
2411            sql_ms.starts_with(r#"[id" OR 1=1--] ="#),
2412            "mssql did not quote col; got: {sql_ms}"
2413        );
2414    }
2415
2416    #[test]
2417    fn to_sql_quotes_in_list_column_names() {
2418        use crate::dialect::Postgres;
2419        let filter = Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]);
2420        let (sql, _) = filter.to_sql(0, &Postgres);
2421        assert!(
2422            sql.starts_with(r#""id" IN ("#),
2423            "expected quoted id on IN, got: {sql}"
2424        );
2425    }
2426
2427    #[test]
2428    fn to_sql_quotes_null_checks() {
2429        use crate::dialect::Postgres;
2430        let filter = Filter::IsNull("deleted_at".into());
2431        let (sql, _) = filter.to_sql(0, &Postgres);
2432        assert_eq!(sql, r#""deleted_at" IS NULL"#);
2433    }
2434
2435    #[test]
2436    fn to_sql_quotes_comparison_operators() {
2437        use crate::dialect::Postgres;
2438
2439        let filter = Filter::Lt("age".into(), FilterValue::Int(18));
2440        let (sql, _) = filter.to_sql(0, &Postgres);
2441        assert!(sql.starts_with(r#""age" < "#), "Lt not quoted: {sql}");
2442
2443        let filter = Filter::Lte("price".into(), FilterValue::Int(100));
2444        let (sql, _) = filter.to_sql(0, &Postgres);
2445        assert!(sql.starts_with(r#""price" <= "#), "Lte not quoted: {sql}");
2446
2447        let filter = Filter::Gt("score".into(), FilterValue::Int(0));
2448        let (sql, _) = filter.to_sql(0, &Postgres);
2449        assert!(sql.starts_with(r#""score" > "#), "Gt not quoted: {sql}");
2450
2451        let filter = Filter::Gte("quantity".into(), FilterValue::Int(1));
2452        let (sql, _) = filter.to_sql(0, &Postgres);
2453        assert!(
2454            sql.starts_with(r#""quantity" >= "#),
2455            "Gte not quoted: {sql}"
2456        );
2457
2458        let filter = Filter::NotEquals("status".into(), "deleted".into());
2459        let (sql, _) = filter.to_sql(0, &Postgres);
2460        assert!(
2461            sql.starts_with(r#""status" != "#),
2462            "NotEquals not quoted: {sql}"
2463        );
2464    }
2465
2466    #[test]
2467    fn to_sql_quotes_like_operators() {
2468        use crate::dialect::Postgres;
2469
2470        let filter = Filter::Contains("email".into(), "example".into());
2471        let (sql, _) = filter.to_sql(0, &Postgres);
2472        assert!(
2473            sql.starts_with(r#""email" LIKE "#),
2474            "Contains not quoted: {sql}"
2475        );
2476
2477        let filter = Filter::StartsWith("name".into(), "admin".into());
2478        let (sql, _) = filter.to_sql(0, &Postgres);
2479        assert!(
2480            sql.starts_with(r#""name" LIKE "#),
2481            "StartsWith not quoted: {sql}"
2482        );
2483
2484        let filter = Filter::EndsWith("domain".into(), ".com".into());
2485        let (sql, _) = filter.to_sql(0, &Postgres);
2486        assert!(
2487            sql.starts_with(r#""domain" LIKE "#),
2488            "EndsWith not quoted: {sql}"
2489        );
2490    }
2491
2492    #[test]
2493    fn to_sql_quotes_not_in() {
2494        use crate::dialect::Postgres;
2495        let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
2496        let (sql, _) = filter.to_sql(0, &Postgres);
2497        assert!(
2498            sql.starts_with(r#""status" NOT IN ("#),
2499            "NotIn not quoted: {sql}"
2500        );
2501    }
2502
2503    #[test]
2504    fn to_sql_quotes_is_not_null() {
2505        use crate::dialect::Postgres;
2506        let filter = Filter::IsNotNull("verified_at".into());
2507        let (sql, _) = filter.to_sql(0, &Postgres);
2508        assert_eq!(sql, r#""verified_at" IS NOT NULL"#);
2509    }
2510
2511    #[test]
2512    fn filter_value_from_u64_in_range() {
2513        assert_eq!(FilterValue::from(42u64), FilterValue::Int(42));
2514        assert_eq!(FilterValue::from(0u64), FilterValue::Int(0));
2515        let max_safe = i64::MAX as u64;
2516        assert_eq!(FilterValue::from(max_safe), FilterValue::Int(i64::MAX));
2517    }
2518
2519    #[test]
2520    #[should_panic(expected = "u64 value exceeds i64::MAX")]
2521    fn filter_value_from_u64_overflow_panics() {
2522        let _ = FilterValue::from(u64::MAX);
2523    }
2524
2525    #[test]
2526    fn filter_value_from_chrono_datetime_utc_rfc3339() {
2527        use chrono::{TimeZone, Utc};
2528        let dt = Utc.with_ymd_and_hms(2020, 1, 15, 10, 30, 45).unwrap();
2529        let fv = FilterValue::from(dt);
2530        assert_eq!(
2531            fv,
2532            FilterValue::String("2020-01-15T10:30:45.000000Z".to_string())
2533        );
2534    }
2535
2536    #[test]
2537    fn filter_value_from_chrono_naive_datetime_iso() {
2538        use chrono::NaiveDate;
2539        let dt = NaiveDate::from_ymd_opt(2020, 1, 15)
2540            .unwrap()
2541            .and_hms_opt(10, 30, 45)
2542            .unwrap();
2543        let fv = FilterValue::from(dt);
2544        assert_eq!(
2545            fv,
2546            FilterValue::String("2020-01-15T10:30:45.000000".to_string())
2547        );
2548    }
2549
2550    #[test]
2551    fn filter_value_from_chrono_naive_date() {
2552        use chrono::NaiveDate;
2553        let d = NaiveDate::from_ymd_opt(2020, 1, 15).unwrap();
2554        assert_eq!(
2555            FilterValue::from(d),
2556            FilterValue::String("2020-01-15".to_string())
2557        );
2558    }
2559
2560    #[test]
2561    fn filter_value_from_chrono_naive_time() {
2562        use chrono::NaiveTime;
2563        let t = NaiveTime::from_hms_opt(10, 30, 45).unwrap();
2564        assert_eq!(
2565            FilterValue::from(t),
2566            FilterValue::String("10:30:45.000000".to_string())
2567        );
2568    }
2569
2570    // ==================== Extended From-impl coverage ====================
2571    // Pins the tail of From<T> for FilterValue impls that weren't previously
2572    // exercised. Each test guards against a specific regression a driver
2573    // would surface downstream — wrong format, wrong variant, or silent
2574    // precision loss.
2575
2576    #[test]
2577    fn filter_value_from_uuid_is_lowercase_hyphenated() {
2578        // Driver bridges (Postgres/MySQL/SQLite/MSSQL) all receive the
2579        // 36-char hyphenated lowercase form; pinning it here prevents a
2580        // hypothetical switch to simple/hyphen-less encoding from silently
2581        // breaking every WHERE uuid_col = $1 binding.
2582        use uuid::Uuid;
2583        let u = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
2584        match FilterValue::from(u) {
2585            FilterValue::String(ref s) => {
2586                assert_eq!(s, "550e8400-e29b-41d4-a716-446655440000");
2587                assert_eq!(s, &u.to_string());
2588            }
2589            other => panic!("expected FilterValue::String, got {other:?}"),
2590        }
2591    }
2592
2593    #[test]
2594    fn filter_value_from_uuid_nil_round_trips() {
2595        use uuid::Uuid;
2596        let u = Uuid::nil();
2597        assert_eq!(
2598            FilterValue::from(u),
2599            FilterValue::String("00000000-0000-0000-0000-000000000000".to_string())
2600        );
2601    }
2602
2603    #[test]
2604    fn filter_value_from_decimal_uses_to_string_not_f64() {
2605        // Critical: Decimal must NOT round-trip via f64. Using to_string()
2606        // preserves precision that parsing-to-f64 loses. "3.14" stays "3.14",
2607        // not "3.1400000000000001".
2608        use rust_decimal::Decimal;
2609        use std::str::FromStr;
2610        let d = Decimal::from_str("3.14").unwrap();
2611        assert_eq!(
2612            FilterValue::from(d),
2613            FilterValue::String("3.14".to_string())
2614        );
2615    }
2616
2617    #[test]
2618    fn filter_value_from_decimal_high_precision_preserved() {
2619        use rust_decimal::Decimal;
2620        use std::str::FromStr;
2621        // 28-digit mantissa — would lose precision through f64.
2622        let d = Decimal::from_str("1234567890.1234567890").unwrap();
2623        match FilterValue::from(d) {
2624            FilterValue::String(ref s) => {
2625                assert_eq!(s, "1234567890.1234567890");
2626            }
2627            other => panic!("expected FilterValue::String, got {other:?}"),
2628        }
2629    }
2630
2631    #[test]
2632    fn filter_value_from_serde_json_value_keeps_json_variant() {
2633        let v = serde_json::json!({"key": "value", "nested": [1, 2, 3]});
2634        match FilterValue::from(v.clone()) {
2635            FilterValue::Json(inner) => {
2636                assert_eq!(inner, v);
2637            }
2638            other => panic!("expected FilterValue::Json, got {other:?}"),
2639        }
2640    }
2641
2642    #[test]
2643    fn filter_value_from_serde_json_null_keeps_json_variant() {
2644        // `serde_json::Value::Null` must land as FilterValue::Json(Null),
2645        // NOT FilterValue::Null — the JSON variant signals to the dialect
2646        // bridge that this column wants JSONB/JSON binding semantics, not
2647        // SQL NULL.
2648        let v = serde_json::Value::Null;
2649        match FilterValue::from(v) {
2650            FilterValue::Json(serde_json::Value::Null) => {}
2651            other => panic!("expected FilterValue::Json(Null), got {other:?}"),
2652        }
2653    }
2654
2655    #[test]
2656    fn filter_value_from_option_none_maps_to_null() {
2657        // Repeats an existing test at a different call site — this is the
2658        // "all integer widths flow through the same Option impl" guard.
2659        let none_i32: Option<i32> = None;
2660        assert_eq!(FilterValue::from(none_i32), FilterValue::Null);
2661        let none_string: Option<String> = None;
2662        assert_eq!(FilterValue::from(none_string), FilterValue::Null);
2663    }
2664
2665    #[test]
2666    fn filter_value_from_signed_integer_extremes() {
2667        // Every integer width widens to Int(i64). Pinning MIN catches sign
2668        // extension bugs (e.g. if `v as i64` were replaced with `v as u64 as i64`).
2669        assert_eq!(FilterValue::from(i8::MIN), FilterValue::Int(i8::MIN as i64));
2670        assert_eq!(FilterValue::from(i8::MAX), FilterValue::Int(i8::MAX as i64));
2671        assert_eq!(
2672            FilterValue::from(i16::MIN),
2673            FilterValue::Int(i16::MIN as i64)
2674        );
2675        assert_eq!(
2676            FilterValue::from(i16::MAX),
2677            FilterValue::Int(i16::MAX as i64)
2678        );
2679    }
2680
2681    #[test]
2682    fn filter_value_from_unsigned_integer_extremes() {
2683        // u8/u16/u32 all fit in i64 so these never panic. u64::MAX has its
2684        // own dedicated `#[should_panic]` test at filter_value_from_u64_overflow_panics.
2685        assert_eq!(FilterValue::from(u8::MAX), FilterValue::Int(u8::MAX as i64));
2686        assert_eq!(
2687            FilterValue::from(u16::MAX),
2688            FilterValue::Int(u16::MAX as i64)
2689        );
2690        assert_eq!(
2691            FilterValue::from(u32::MAX),
2692            FilterValue::Int(u32::MAX as i64)
2693        );
2694        // u32::MAX = 4_294_967_295, well below i64::MAX.
2695        assert_eq!(FilterValue::from(u32::MAX), FilterValue::Int(4_294_967_295));
2696    }
2697
2698    #[test]
2699    fn filter_value_from_f32_widens_to_f64() {
2700        // f32 -> f64 widening must happen via `f64::from(v)`, NOT `v as f64`
2701        // — the cast form is fine for IEEE-754 normal values but we pin it
2702        // here to document intent. 1.5f32 is exactly representable so no
2703        // precision loss either way.
2704        let v: f32 = 1.5;
2705        assert_eq!(FilterValue::from(v), FilterValue::Float(1.5));
2706    }
2707
2708    // ==================== ToFilterValue tests ====================
2709    // These pin the reverse-of-FromColumn projection used by the relation
2710    // loader and `ModelWithPk`. Each case guards against a drift from the
2711    // matching `From<T>` impl above; the relation executor relies on them
2712    // producing byte-identical values to the parameter-binding path.
2713
2714    #[test]
2715    fn to_filter_value_option_some_some() {
2716        let v: Option<i32> = Some(42);
2717        assert_eq!(v.to_filter_value(), FilterValue::Int(42));
2718    }
2719
2720    #[test]
2721    fn to_filter_value_option_none_is_null() {
2722        let v: Option<i32> = None;
2723        assert_eq!(v.to_filter_value(), FilterValue::Null);
2724    }
2725
2726    #[test]
2727    fn to_filter_value_uuid_is_string() {
2728        let id = uuid::Uuid::nil();
2729        assert_eq!(id.to_filter_value(), FilterValue::String(id.to_string()));
2730    }
2731
2732    #[test]
2733    fn to_filter_value_bool_is_bool() {
2734        assert_eq!(true.to_filter_value(), FilterValue::Bool(true));
2735    }
2736}