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    #[allow(clippy::approx_constant)]
1717    fn test_filter_value_from_f64() {
1718        assert_eq!(FilterValue::from(3.14f64), FilterValue::Float(3.14));
1719    }
1720
1721    #[test]
1722    fn test_filter_value_from_string() {
1723        assert_eq!(
1724            FilterValue::from("hello".to_string()),
1725            FilterValue::String("hello".to_string())
1726        );
1727    }
1728
1729    #[test]
1730    fn test_filter_value_from_vec() {
1731        let values: Vec<i32> = vec![1, 2, 3];
1732        let filter_val: FilterValue = values.into();
1733        if let FilterValue::List(list) = filter_val {
1734            assert_eq!(list.len(), 3);
1735            assert_eq!(list[0], FilterValue::Int(1));
1736            assert_eq!(list[1], FilterValue::Int(2));
1737            assert_eq!(list[2], FilterValue::Int(3));
1738        } else {
1739            panic!("Expected List");
1740        }
1741    }
1742
1743    #[test]
1744    fn test_filter_value_from_option_some() {
1745        let val: FilterValue = Some(42i32).into();
1746        assert_eq!(val, FilterValue::Int(42));
1747    }
1748
1749    #[test]
1750    fn test_filter_value_from_option_none() {
1751        let val: FilterValue = Option::<i32>::None.into();
1752        assert_eq!(val, FilterValue::Null);
1753    }
1754
1755    // ==================== ScalarFilter Tests ====================
1756
1757    #[test]
1758    fn test_scalar_filter_not() {
1759        let filter = ScalarFilter::Not(Box::new("test".to_string())).into_filter("name");
1760        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1761        assert_eq!(sql, r#""name" != $1"#);
1762        assert_eq!(params.len(), 1);
1763    }
1764
1765    #[test]
1766    fn test_scalar_filter_in() {
1767        let filter = ScalarFilter::In(vec!["a".to_string(), "b".to_string()]).into_filter("status");
1768        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1769        assert!(sql.contains("IN"));
1770        assert_eq!(params.len(), 2);
1771    }
1772
1773    #[test]
1774    fn test_scalar_filter_not_in() {
1775        let filter = ScalarFilter::NotIn(vec!["x".to_string()]).into_filter("status");
1776        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1777        assert!(sql.contains("NOT IN"));
1778        assert_eq!(params.len(), 1);
1779    }
1780
1781    #[test]
1782    fn test_scalar_filter_lt() {
1783        let filter = ScalarFilter::Lt(100i32).into_filter("price");
1784        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1785        assert_eq!(sql, r#""price" < $1"#);
1786        assert_eq!(params.len(), 1);
1787    }
1788
1789    #[test]
1790    fn test_scalar_filter_lte() {
1791        let filter = ScalarFilter::Lte(100i32).into_filter("price");
1792        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1793        assert_eq!(sql, r#""price" <= $1"#);
1794        assert_eq!(params.len(), 1);
1795    }
1796
1797    #[test]
1798    fn test_scalar_filter_gt() {
1799        let filter = ScalarFilter::Gt(0i32).into_filter("quantity");
1800        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1801        assert_eq!(sql, r#""quantity" > $1"#);
1802        assert_eq!(params.len(), 1);
1803    }
1804
1805    #[test]
1806    fn test_scalar_filter_gte() {
1807        let filter = ScalarFilter::Gte(0i32).into_filter("quantity");
1808        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1809        assert_eq!(sql, r#""quantity" >= $1"#);
1810        assert_eq!(params.len(), 1);
1811    }
1812
1813    #[test]
1814    fn test_scalar_filter_starts_with() {
1815        let filter = ScalarFilter::StartsWith("prefix".to_string()).into_filter("name");
1816        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1817        assert!(sql.contains("LIKE"));
1818        assert_eq!(params.len(), 1);
1819        if let FilterValue::String(s) = &params[0] {
1820            assert!(s.starts_with("prefix"));
1821            assert!(s.ends_with("%"));
1822        }
1823    }
1824
1825    #[test]
1826    fn test_scalar_filter_ends_with() {
1827        let filter = ScalarFilter::EndsWith("suffix".to_string()).into_filter("name");
1828        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1829        assert!(sql.contains("LIKE"));
1830        assert_eq!(params.len(), 1);
1831        if let FilterValue::String(s) = &params[0] {
1832            assert!(s.starts_with("%"));
1833            assert!(s.ends_with("suffix"));
1834        }
1835    }
1836
1837    #[test]
1838    fn test_scalar_filter_is_null() {
1839        let filter = ScalarFilter::<String>::IsNull.into_filter("deleted_at");
1840        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1841        assert_eq!(sql, r#""deleted_at" IS NULL"#);
1842        assert!(params.is_empty());
1843    }
1844
1845    #[test]
1846    fn test_scalar_filter_is_not_null() {
1847        let filter = ScalarFilter::<String>::IsNotNull.into_filter("name");
1848        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1849        assert_eq!(sql, r#""name" IS NOT NULL"#);
1850        assert!(params.is_empty());
1851    }
1852
1853    // ==================== Filter Tests ====================
1854
1855    #[test]
1856    fn test_filter_none() {
1857        let filter = Filter::none();
1858        assert!(filter.is_none());
1859        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1860        assert_eq!(sql, "TRUE"); // Filter::None generates TRUE
1861        assert!(params.is_empty());
1862    }
1863
1864    #[test]
1865    fn test_filter_not_equals() {
1866        let filter = Filter::NotEquals("status".into(), "deleted".into());
1867        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1868        assert_eq!(sql, r#""status" != $1"#);
1869        assert_eq!(params.len(), 1);
1870    }
1871
1872    #[test]
1873    fn test_filter_lte() {
1874        let filter = Filter::Lte("price".into(), FilterValue::Int(100));
1875        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1876        assert_eq!(sql, r#""price" <= $1"#);
1877        assert_eq!(params.len(), 1);
1878    }
1879
1880    #[test]
1881    fn test_filter_gte() {
1882        let filter = Filter::Gte("quantity".into(), FilterValue::Int(0));
1883        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1884        assert_eq!(sql, r#""quantity" >= $1"#);
1885        assert_eq!(params.len(), 1);
1886    }
1887
1888    #[test]
1889    fn test_filter_not_in() {
1890        let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
1891        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1892        assert!(sql.contains("NOT IN"));
1893        assert_eq!(params.len(), 2);
1894    }
1895
1896    #[test]
1897    fn test_filter_starts_with() {
1898        let filter = Filter::StartsWith("email".into(), "admin".into());
1899        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1900        assert!(sql.contains("LIKE"));
1901        assert_eq!(params.len(), 1);
1902    }
1903
1904    #[test]
1905    fn test_filter_ends_with() {
1906        let filter = Filter::EndsWith("email".into(), "@example.com".into());
1907        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1908        assert!(sql.contains("LIKE"));
1909        assert_eq!(params.len(), 1);
1910    }
1911
1912    #[test]
1913    fn test_filter_is_not_null() {
1914        let filter = Filter::IsNotNull("name".into());
1915        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1916        assert_eq!(sql, r#""name" IS NOT NULL"#);
1917        assert!(params.is_empty());
1918    }
1919
1920    // ==================== Filter Combination Tests ====================
1921
1922    #[test]
1923    fn test_filter_and_empty() {
1924        let filter = Filter::and([]);
1925        assert!(filter.is_none());
1926    }
1927
1928    #[test]
1929    fn test_filter_and_single() {
1930        let f = Filter::Equals("name".into(), "Alice".into());
1931        let combined = Filter::and([f.clone()]);
1932        assert_eq!(combined, f);
1933    }
1934
1935    #[test]
1936    fn test_filter_and_with_none() {
1937        let f1 = Filter::Equals("name".into(), "Alice".into());
1938        let f2 = Filter::None;
1939        let combined = Filter::and([f1.clone(), f2]);
1940        assert_eq!(combined, f1);
1941    }
1942
1943    #[test]
1944    fn test_filter_or_empty() {
1945        let filter = Filter::or([]);
1946        assert!(filter.is_none());
1947    }
1948
1949    #[test]
1950    fn test_filter_or_single() {
1951        let f = Filter::Equals("status".into(), "active".into());
1952        let combined = Filter::or([f.clone()]);
1953        assert_eq!(combined, f);
1954    }
1955
1956    #[test]
1957    fn test_filter_or_with_none() {
1958        let f1 = Filter::Equals("status".into(), "active".into());
1959        let f2 = Filter::None;
1960        let combined = Filter::or([f1.clone(), f2]);
1961        assert_eq!(combined, f1);
1962    }
1963
1964    #[test]
1965    fn test_filter_not_none() {
1966        let filter = Filter::not(Filter::None);
1967        assert!(filter.is_none());
1968    }
1969
1970    #[test]
1971    fn test_filter_and_then() {
1972        let f1 = Filter::Equals("name".into(), "Alice".into());
1973        let f2 = Filter::Gt("age".into(), FilterValue::Int(18));
1974        let combined = f1.and_then(f2);
1975
1976        let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
1977        assert!(sql.contains("AND"));
1978        assert_eq!(params.len(), 2);
1979    }
1980
1981    #[test]
1982    fn test_filter_and_then_with_none_first() {
1983        let f1 = Filter::None;
1984        let f2 = Filter::Equals("name".into(), "Bob".into());
1985        let combined = f1.and_then(f2.clone());
1986        assert_eq!(combined, f2);
1987    }
1988
1989    #[test]
1990    fn test_filter_and_then_with_none_second() {
1991        let f1 = Filter::Equals("name".into(), "Alice".into());
1992        let f2 = Filter::None;
1993        let combined = f1.clone().and_then(f2);
1994        assert_eq!(combined, f1);
1995    }
1996
1997    #[test]
1998    fn test_filter_and_then_chained() {
1999        let f1 = Filter::Equals("a".into(), "1".into());
2000        let f2 = Filter::Equals("b".into(), "2".into());
2001        let f3 = Filter::Equals("c".into(), "3".into());
2002        let combined = f1.and_then(f2).and_then(f3);
2003
2004        let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
2005        assert!(sql.contains("AND"));
2006        assert_eq!(params.len(), 3);
2007    }
2008
2009    #[test]
2010    fn test_filter_or_else() {
2011        let f1 = Filter::Equals("status".into(), "active".into());
2012        let f2 = Filter::Equals("status".into(), "pending".into());
2013        let combined = f1.or_else(f2);
2014
2015        let (sql, _) = combined.to_sql(0, &crate::dialect::Postgres);
2016        assert!(sql.contains("OR"));
2017    }
2018
2019    #[test]
2020    fn test_filter_or_else_with_none_first() {
2021        let f1 = Filter::None;
2022        let f2 = Filter::Equals("name".into(), "Bob".into());
2023        let combined = f1.or_else(f2.clone());
2024        assert_eq!(combined, f2);
2025    }
2026
2027    #[test]
2028    fn test_filter_or_else_with_none_second() {
2029        let f1 = Filter::Equals("name".into(), "Alice".into());
2030        let f2 = Filter::None;
2031        let combined = f1.clone().or_else(f2);
2032        assert_eq!(combined, f1);
2033    }
2034
2035    // ==================== Complex Filter SQL Generation ====================
2036
2037    #[test]
2038    fn test_filter_nested_and_or() {
2039        let f1 = Filter::Equals("status".into(), "active".into());
2040        let f2 = Filter::and([
2041            Filter::Gt("age".into(), FilterValue::Int(18)),
2042            Filter::Lt("age".into(), FilterValue::Int(65)),
2043        ]);
2044        let combined = Filter::and([f1, f2]);
2045
2046        let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
2047        assert!(sql.contains("AND"));
2048        assert_eq!(params.len(), 3);
2049    }
2050
2051    #[test]
2052    fn test_filter_nested_not() {
2053        let inner = Filter::and([
2054            Filter::Equals("status".into(), "deleted".into()),
2055            Filter::Equals("archived".into(), FilterValue::Bool(true)),
2056        ]);
2057        let filter = Filter::not(inner);
2058
2059        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2060        assert!(sql.contains("NOT"));
2061        assert!(sql.contains("AND"));
2062        assert_eq!(params.len(), 2);
2063    }
2064
2065    #[test]
2066    fn test_filter_with_json_value() {
2067        let json_val = serde_json::json!({"key": "value"});
2068        let filter = Filter::Equals("metadata".into(), FilterValue::Json(json_val));
2069        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2070        assert_eq!(sql, r#""metadata" = $1"#);
2071        assert_eq!(params.len(), 1);
2072    }
2073
2074    #[test]
2075    fn test_filter_in_empty_list() {
2076        let filter = Filter::In("status".into(), vec![]);
2077        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2078        // Empty IN generates FALSE (no match possible)
2079        assert!(
2080            sql.contains("FALSE")
2081                || sql.contains("1=0")
2082                || sql.is_empty()
2083                || sql.contains("status")
2084        );
2085        assert!(params.is_empty());
2086    }
2087
2088    #[test]
2089    fn test_filter_with_null_value() {
2090        // When filtering with Null value, it uses IS NULL instead of = $1
2091        let filter = Filter::IsNull("deleted_at".into());
2092        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2093        assert!(sql.contains("deleted_at"));
2094        assert!(sql.contains("IS NULL"));
2095        assert!(params.is_empty());
2096    }
2097
2098    // ==================== Builder Tests ====================
2099
2100    #[test]
2101    fn test_and_builder_basic() {
2102        let filter = Filter::and_builder(3)
2103            .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2104            .push(Filter::Gt("score".into(), FilterValue::Int(100)))
2105            .push(Filter::IsNotNull("email".into()))
2106            .build();
2107
2108        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2109        assert!(sql.contains("AND"));
2110        assert_eq!(params.len(), 2); // score and active, IS NOT NULL has no param
2111    }
2112
2113    #[test]
2114    fn test_and_builder_empty() {
2115        let filter = Filter::and_builder(0).build();
2116        assert!(filter.is_none());
2117    }
2118
2119    #[test]
2120    fn test_and_builder_single() {
2121        let filter = Filter::and_builder(1)
2122            .push(Filter::Equals("id".into(), FilterValue::Int(42)))
2123            .build();
2124
2125        // Single filter should not be wrapped in AND
2126        assert!(matches!(filter, Filter::Equals(_, _)));
2127    }
2128
2129    #[test]
2130    fn test_and_builder_filters_none() {
2131        let filter = Filter::and_builder(3)
2132            .push(Filter::None)
2133            .push(Filter::Equals("id".into(), FilterValue::Int(1)))
2134            .push(Filter::None)
2135            .build();
2136
2137        // None filters should be filtered out, leaving single filter
2138        assert!(matches!(filter, Filter::Equals(_, _)));
2139    }
2140
2141    #[test]
2142    fn test_and_builder_push_if() {
2143        let include_deleted = false;
2144        let filter = Filter::and_builder(2)
2145            .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2146            .push_if(include_deleted, Filter::IsNull("deleted_at".into()))
2147            .build();
2148
2149        // Should only have active filter since include_deleted is false
2150        assert!(matches!(filter, Filter::Equals(_, _)));
2151    }
2152
2153    #[test]
2154    fn test_or_builder_basic() {
2155        let filter = Filter::or_builder(2)
2156            .push(Filter::Equals(
2157                "role".into(),
2158                FilterValue::String("admin".into()),
2159            ))
2160            .push(Filter::Equals(
2161                "role".into(),
2162                FilterValue::String("moderator".into()),
2163            ))
2164            .build();
2165
2166        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2167        assert!(sql.contains("OR"));
2168    }
2169
2170    #[test]
2171    fn test_or_builder_empty() {
2172        let filter = Filter::or_builder(0).build();
2173        assert!(filter.is_none());
2174    }
2175
2176    #[test]
2177    fn test_or_builder_single() {
2178        let filter = Filter::or_builder(1)
2179            .push(Filter::Equals("id".into(), FilterValue::Int(42)))
2180            .build();
2181
2182        // Single filter should not be wrapped in OR
2183        assert!(matches!(filter, Filter::Equals(_, _)));
2184    }
2185
2186    #[test]
2187    fn test_fluent_builder_and() {
2188        let filter = Filter::builder()
2189            .eq("status", "active")
2190            .gt("age", 18)
2191            .is_not_null("email")
2192            .build_and();
2193
2194        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2195        assert!(sql.contains("AND"));
2196        assert_eq!(params.len(), 2);
2197    }
2198
2199    #[test]
2200    fn test_fluent_builder_or() {
2201        let filter = Filter::builder()
2202            .eq("role", "admin")
2203            .eq("role", "moderator")
2204            .build_or();
2205
2206        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2207        assert!(sql.contains("OR"));
2208    }
2209
2210    #[test]
2211    fn test_fluent_builder_with_capacity() {
2212        let filter = Filter::builder()
2213            .with_capacity(5)
2214            .eq("a", 1)
2215            .ne("b", 2)
2216            .lt("c", 3)
2217            .lte("d", 4)
2218            .gte("e", 5)
2219            .build_and();
2220
2221        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2222        assert!(sql.contains("AND"));
2223        assert_eq!(params.len(), 5);
2224    }
2225
2226    #[test]
2227    fn test_fluent_builder_string_operations() {
2228        let filter = Filter::builder()
2229            .contains("name", "john")
2230            .starts_with("email", "admin")
2231            .ends_with("domain", ".com")
2232            .build_and();
2233
2234        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2235        assert!(sql.contains("LIKE"));
2236    }
2237
2238    #[test]
2239    fn test_fluent_builder_null_operations() {
2240        let filter = Filter::builder()
2241            .is_null("deleted_at")
2242            .is_not_null("created_at")
2243            .build_and();
2244
2245        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2246        assert!(sql.contains("IS NULL"));
2247        assert!(sql.contains("IS NOT NULL"));
2248    }
2249
2250    #[test]
2251    fn test_fluent_builder_in_operations() {
2252        let filter = Filter::builder()
2253            .is_in("status", vec!["pending", "processing"])
2254            .not_in("role", vec!["banned", "suspended"])
2255            .build_and();
2256
2257        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2258        assert!(sql.contains("IN"));
2259        assert!(sql.contains("NOT IN"));
2260    }
2261
2262    #[test]
2263    fn test_fluent_builder_filter_if() {
2264        let include_archived = false;
2265        let filter = Filter::builder()
2266            .eq("active", true)
2267            .filter_if(
2268                include_archived,
2269                Filter::Equals("archived".into(), FilterValue::Bool(true)),
2270            )
2271            .build_and();
2272
2273        // Should only have active filter
2274        assert!(matches!(filter, Filter::Equals(_, _)));
2275    }
2276
2277    #[test]
2278    fn test_fluent_builder_filter_if_some() {
2279        let maybe_status: Option<Filter> = Some(Filter::Equals("status".into(), "active".into()));
2280        let filter = Filter::builder()
2281            .eq("id", 1)
2282            .filter_if_some(maybe_status)
2283            .build_and();
2284
2285        assert!(matches!(filter, Filter::And(_)));
2286    }
2287
2288    #[test]
2289    fn test_and_builder_extend() {
2290        let extra_filters = vec![
2291            Filter::Gt("score".into(), FilterValue::Int(100)),
2292            Filter::Lt("score".into(), FilterValue::Int(1000)),
2293        ];
2294
2295        let filter = Filter::and_builder(3)
2296            .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2297            .extend(extra_filters)
2298            .build();
2299
2300        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2301        assert!(sql.contains("AND"));
2302        assert_eq!(params.len(), 3);
2303    }
2304
2305    #[test]
2306    fn test_builder_len_and_is_empty() {
2307        let mut builder = AndFilterBuilder::new();
2308        assert!(builder.is_empty());
2309        assert_eq!(builder.len(), 0);
2310
2311        builder = builder.push(Filter::Equals("id".into(), FilterValue::Int(1)));
2312        assert!(!builder.is_empty());
2313        assert_eq!(builder.len(), 1);
2314    }
2315
2316    // ==================== and2/or2 Tests ====================
2317
2318    #[test]
2319    fn test_and2_both_valid() {
2320        let a = Filter::Equals("id".into(), FilterValue::Int(1));
2321        let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2322        let filter = Filter::and2(a, b);
2323
2324        assert!(matches!(filter, Filter::And(_)));
2325        let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2326        assert!(sql.contains("AND"));
2327        assert_eq!(params.len(), 2);
2328    }
2329
2330    #[test]
2331    fn test_and2_first_none() {
2332        let a = Filter::None;
2333        let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2334        let filter = Filter::and2(a, b.clone());
2335
2336        assert_eq!(filter, b);
2337    }
2338
2339    #[test]
2340    fn test_and2_second_none() {
2341        let a = Filter::Equals("id".into(), FilterValue::Int(1));
2342        let b = Filter::None;
2343        let filter = Filter::and2(a.clone(), b);
2344
2345        assert_eq!(filter, a);
2346    }
2347
2348    #[test]
2349    fn test_and2_both_none() {
2350        let filter = Filter::and2(Filter::None, Filter::None);
2351        assert!(filter.is_none());
2352    }
2353
2354    #[test]
2355    fn test_or2_both_valid() {
2356        let a = Filter::Equals("role".into(), FilterValue::String("admin".into()));
2357        let b = Filter::Equals("role".into(), FilterValue::String("mod".into()));
2358        let filter = Filter::or2(a, b);
2359
2360        assert!(matches!(filter, Filter::Or(_)));
2361        let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2362        assert!(sql.contains("OR"));
2363    }
2364
2365    #[test]
2366    fn test_or2_first_none() {
2367        let a = Filter::None;
2368        let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2369        let filter = Filter::or2(a, b.clone());
2370
2371        assert_eq!(filter, b);
2372    }
2373
2374    #[test]
2375    fn test_or2_second_none() {
2376        let a = Filter::Equals("id".into(), FilterValue::Int(1));
2377        let b = Filter::None;
2378        let filter = Filter::or2(a.clone(), b);
2379
2380        assert_eq!(filter, a);
2381    }
2382
2383    #[test]
2384    fn test_or2_both_none() {
2385        let filter = Filter::or2(Filter::None, Filter::None);
2386        assert!(filter.is_none());
2387    }
2388
2389    // ==================== SQL Injection Prevention Tests ====================
2390
2391    #[test]
2392    fn to_sql_quotes_column_names_against_injection() {
2393        use crate::dialect::{Mssql, Mysql, Postgres};
2394
2395        // Malicious column name attempts to break out of the identifier.
2396        let filter = Filter::Equals(r#"id" OR 1=1--"#.into(), FilterValue::Int(1));
2397
2398        let (sql_pg, _) = filter.to_sql(0, &Postgres);
2399        assert!(
2400            sql_pg.starts_with(r#""id"" OR 1=1--" ="#),
2401            "postgres did not quote col; got: {sql_pg}"
2402        );
2403
2404        let (sql_my, _) = filter.to_sql(0, &Mysql);
2405        assert!(
2406            sql_my.starts_with(r#"`id" OR 1=1--` ="#),
2407            "mysql did not quote col; got: {sql_my}"
2408        );
2409
2410        let (sql_ms, _) = filter.to_sql(0, &Mssql);
2411        assert!(
2412            sql_ms.starts_with(r#"[id" OR 1=1--] ="#),
2413            "mssql did not quote col; got: {sql_ms}"
2414        );
2415    }
2416
2417    #[test]
2418    fn to_sql_quotes_in_list_column_names() {
2419        use crate::dialect::Postgres;
2420        let filter = Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]);
2421        let (sql, _) = filter.to_sql(0, &Postgres);
2422        assert!(
2423            sql.starts_with(r#""id" IN ("#),
2424            "expected quoted id on IN, got: {sql}"
2425        );
2426    }
2427
2428    #[test]
2429    fn to_sql_quotes_null_checks() {
2430        use crate::dialect::Postgres;
2431        let filter = Filter::IsNull("deleted_at".into());
2432        let (sql, _) = filter.to_sql(0, &Postgres);
2433        assert_eq!(sql, r#""deleted_at" IS NULL"#);
2434    }
2435
2436    #[test]
2437    fn to_sql_quotes_comparison_operators() {
2438        use crate::dialect::Postgres;
2439
2440        let filter = Filter::Lt("age".into(), FilterValue::Int(18));
2441        let (sql, _) = filter.to_sql(0, &Postgres);
2442        assert!(sql.starts_with(r#""age" < "#), "Lt not quoted: {sql}");
2443
2444        let filter = Filter::Lte("price".into(), FilterValue::Int(100));
2445        let (sql, _) = filter.to_sql(0, &Postgres);
2446        assert!(sql.starts_with(r#""price" <= "#), "Lte not quoted: {sql}");
2447
2448        let filter = Filter::Gt("score".into(), FilterValue::Int(0));
2449        let (sql, _) = filter.to_sql(0, &Postgres);
2450        assert!(sql.starts_with(r#""score" > "#), "Gt not quoted: {sql}");
2451
2452        let filter = Filter::Gte("quantity".into(), FilterValue::Int(1));
2453        let (sql, _) = filter.to_sql(0, &Postgres);
2454        assert!(
2455            sql.starts_with(r#""quantity" >= "#),
2456            "Gte not quoted: {sql}"
2457        );
2458
2459        let filter = Filter::NotEquals("status".into(), "deleted".into());
2460        let (sql, _) = filter.to_sql(0, &Postgres);
2461        assert!(
2462            sql.starts_with(r#""status" != "#),
2463            "NotEquals not quoted: {sql}"
2464        );
2465    }
2466
2467    #[test]
2468    fn to_sql_quotes_like_operators() {
2469        use crate::dialect::Postgres;
2470
2471        let filter = Filter::Contains("email".into(), "example".into());
2472        let (sql, _) = filter.to_sql(0, &Postgres);
2473        assert!(
2474            sql.starts_with(r#""email" LIKE "#),
2475            "Contains not quoted: {sql}"
2476        );
2477
2478        let filter = Filter::StartsWith("name".into(), "admin".into());
2479        let (sql, _) = filter.to_sql(0, &Postgres);
2480        assert!(
2481            sql.starts_with(r#""name" LIKE "#),
2482            "StartsWith not quoted: {sql}"
2483        );
2484
2485        let filter = Filter::EndsWith("domain".into(), ".com".into());
2486        let (sql, _) = filter.to_sql(0, &Postgres);
2487        assert!(
2488            sql.starts_with(r#""domain" LIKE "#),
2489            "EndsWith not quoted: {sql}"
2490        );
2491    }
2492
2493    #[test]
2494    fn to_sql_quotes_not_in() {
2495        use crate::dialect::Postgres;
2496        let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
2497        let (sql, _) = filter.to_sql(0, &Postgres);
2498        assert!(
2499            sql.starts_with(r#""status" NOT IN ("#),
2500            "NotIn not quoted: {sql}"
2501        );
2502    }
2503
2504    #[test]
2505    fn to_sql_quotes_is_not_null() {
2506        use crate::dialect::Postgres;
2507        let filter = Filter::IsNotNull("verified_at".into());
2508        let (sql, _) = filter.to_sql(0, &Postgres);
2509        assert_eq!(sql, r#""verified_at" IS NOT NULL"#);
2510    }
2511
2512    #[test]
2513    fn filter_value_from_u64_in_range() {
2514        assert_eq!(FilterValue::from(42u64), FilterValue::Int(42));
2515        assert_eq!(FilterValue::from(0u64), FilterValue::Int(0));
2516        let max_safe = i64::MAX as u64;
2517        assert_eq!(FilterValue::from(max_safe), FilterValue::Int(i64::MAX));
2518    }
2519
2520    #[test]
2521    #[should_panic(expected = "u64 value exceeds i64::MAX")]
2522    fn filter_value_from_u64_overflow_panics() {
2523        let _ = FilterValue::from(u64::MAX);
2524    }
2525
2526    #[test]
2527    fn filter_value_from_chrono_datetime_utc_rfc3339() {
2528        use chrono::{TimeZone, Utc};
2529        let dt = Utc.with_ymd_and_hms(2020, 1, 15, 10, 30, 45).unwrap();
2530        let fv = FilterValue::from(dt);
2531        assert_eq!(
2532            fv,
2533            FilterValue::String("2020-01-15T10:30:45.000000Z".to_string())
2534        );
2535    }
2536
2537    #[test]
2538    fn filter_value_from_chrono_naive_datetime_iso() {
2539        use chrono::NaiveDate;
2540        let dt = NaiveDate::from_ymd_opt(2020, 1, 15)
2541            .unwrap()
2542            .and_hms_opt(10, 30, 45)
2543            .unwrap();
2544        let fv = FilterValue::from(dt);
2545        assert_eq!(
2546            fv,
2547            FilterValue::String("2020-01-15T10:30:45.000000".to_string())
2548        );
2549    }
2550
2551    #[test]
2552    fn filter_value_from_chrono_naive_date() {
2553        use chrono::NaiveDate;
2554        let d = NaiveDate::from_ymd_opt(2020, 1, 15).unwrap();
2555        assert_eq!(
2556            FilterValue::from(d),
2557            FilterValue::String("2020-01-15".to_string())
2558        );
2559    }
2560
2561    #[test]
2562    fn filter_value_from_chrono_naive_time() {
2563        use chrono::NaiveTime;
2564        let t = NaiveTime::from_hms_opt(10, 30, 45).unwrap();
2565        assert_eq!(
2566            FilterValue::from(t),
2567            FilterValue::String("10:30:45.000000".to_string())
2568        );
2569    }
2570
2571    // ==================== Extended From-impl coverage ====================
2572    // Pins the tail of From<T> for FilterValue impls that weren't previously
2573    // exercised. Each test guards against a specific regression a driver
2574    // would surface downstream — wrong format, wrong variant, or silent
2575    // precision loss.
2576
2577    #[test]
2578    fn filter_value_from_uuid_is_lowercase_hyphenated() {
2579        // Driver bridges (Postgres/MySQL/SQLite/MSSQL) all receive the
2580        // 36-char hyphenated lowercase form; pinning it here prevents a
2581        // hypothetical switch to simple/hyphen-less encoding from silently
2582        // breaking every WHERE uuid_col = $1 binding.
2583        use uuid::Uuid;
2584        let u = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
2585        match FilterValue::from(u) {
2586            FilterValue::String(ref s) => {
2587                assert_eq!(s, "550e8400-e29b-41d4-a716-446655440000");
2588                assert_eq!(s, &u.to_string());
2589            }
2590            other => panic!("expected FilterValue::String, got {other:?}"),
2591        }
2592    }
2593
2594    #[test]
2595    fn filter_value_from_uuid_nil_round_trips() {
2596        use uuid::Uuid;
2597        let u = Uuid::nil();
2598        assert_eq!(
2599            FilterValue::from(u),
2600            FilterValue::String("00000000-0000-0000-0000-000000000000".to_string())
2601        );
2602    }
2603
2604    #[test]
2605    fn filter_value_from_decimal_uses_to_string_not_f64() {
2606        // Critical: Decimal must NOT round-trip via f64. Using to_string()
2607        // preserves precision that parsing-to-f64 loses. "3.14" stays "3.14",
2608        // not "3.1400000000000001".
2609        use rust_decimal::Decimal;
2610        use std::str::FromStr;
2611        let d = Decimal::from_str("3.14").unwrap();
2612        assert_eq!(
2613            FilterValue::from(d),
2614            FilterValue::String("3.14".to_string())
2615        );
2616    }
2617
2618    #[test]
2619    fn filter_value_from_decimal_high_precision_preserved() {
2620        use rust_decimal::Decimal;
2621        use std::str::FromStr;
2622        // 28-digit mantissa — would lose precision through f64.
2623        let d = Decimal::from_str("1234567890.1234567890").unwrap();
2624        match FilterValue::from(d) {
2625            FilterValue::String(ref s) => {
2626                assert_eq!(s, "1234567890.1234567890");
2627            }
2628            other => panic!("expected FilterValue::String, got {other:?}"),
2629        }
2630    }
2631
2632    #[test]
2633    fn filter_value_from_serde_json_value_keeps_json_variant() {
2634        let v = serde_json::json!({"key": "value", "nested": [1, 2, 3]});
2635        match FilterValue::from(v.clone()) {
2636            FilterValue::Json(inner) => {
2637                assert_eq!(inner, v);
2638            }
2639            other => panic!("expected FilterValue::Json, got {other:?}"),
2640        }
2641    }
2642
2643    #[test]
2644    fn filter_value_from_serde_json_null_keeps_json_variant() {
2645        // `serde_json::Value::Null` must land as FilterValue::Json(Null),
2646        // NOT FilterValue::Null — the JSON variant signals to the dialect
2647        // bridge that this column wants JSONB/JSON binding semantics, not
2648        // SQL NULL.
2649        let v = serde_json::Value::Null;
2650        match FilterValue::from(v) {
2651            FilterValue::Json(serde_json::Value::Null) => {}
2652            other => panic!("expected FilterValue::Json(Null), got {other:?}"),
2653        }
2654    }
2655
2656    #[test]
2657    fn filter_value_from_option_none_maps_to_null() {
2658        // Repeats an existing test at a different call site — this is the
2659        // "all integer widths flow through the same Option impl" guard.
2660        let none_i32: Option<i32> = None;
2661        assert_eq!(FilterValue::from(none_i32), FilterValue::Null);
2662        let none_string: Option<String> = None;
2663        assert_eq!(FilterValue::from(none_string), FilterValue::Null);
2664    }
2665
2666    #[test]
2667    fn filter_value_from_signed_integer_extremes() {
2668        // Every integer width widens to Int(i64). Pinning MIN catches sign
2669        // extension bugs (e.g. if `v as i64` were replaced with `v as u64 as i64`).
2670        assert_eq!(FilterValue::from(i8::MIN), FilterValue::Int(i8::MIN as i64));
2671        assert_eq!(FilterValue::from(i8::MAX), FilterValue::Int(i8::MAX as i64));
2672        assert_eq!(
2673            FilterValue::from(i16::MIN),
2674            FilterValue::Int(i16::MIN as i64)
2675        );
2676        assert_eq!(
2677            FilterValue::from(i16::MAX),
2678            FilterValue::Int(i16::MAX as i64)
2679        );
2680    }
2681
2682    #[test]
2683    fn filter_value_from_unsigned_integer_extremes() {
2684        // u8/u16/u32 all fit in i64 so these never panic. u64::MAX has its
2685        // own dedicated `#[should_panic]` test at filter_value_from_u64_overflow_panics.
2686        assert_eq!(FilterValue::from(u8::MAX), FilterValue::Int(u8::MAX as i64));
2687        assert_eq!(
2688            FilterValue::from(u16::MAX),
2689            FilterValue::Int(u16::MAX as i64)
2690        );
2691        assert_eq!(
2692            FilterValue::from(u32::MAX),
2693            FilterValue::Int(u32::MAX as i64)
2694        );
2695        // u32::MAX = 4_294_967_295, well below i64::MAX.
2696        assert_eq!(FilterValue::from(u32::MAX), FilterValue::Int(4_294_967_295));
2697    }
2698
2699    #[test]
2700    fn filter_value_from_f32_widens_to_f64() {
2701        // f32 -> f64 widening must happen via `f64::from(v)`, NOT `v as f64`
2702        // — the cast form is fine for IEEE-754 normal values but we pin it
2703        // here to document intent. 1.5f32 is exactly representable so no
2704        // precision loss either way.
2705        let v: f32 = 1.5;
2706        assert_eq!(FilterValue::from(v), FilterValue::Float(1.5));
2707    }
2708
2709    // ==================== ToFilterValue tests ====================
2710    // These pin the reverse-of-FromColumn projection used by the relation
2711    // loader and `ModelWithPk`. Each case guards against a drift from the
2712    // matching `From<T>` impl above; the relation executor relies on them
2713    // producing byte-identical values to the parameter-binding path.
2714
2715    #[test]
2716    fn to_filter_value_option_some_some() {
2717        let v: Option<i32> = Some(42);
2718        assert_eq!(v.to_filter_value(), FilterValue::Int(42));
2719    }
2720
2721    #[test]
2722    fn to_filter_value_option_none_is_null() {
2723        let v: Option<i32> = None;
2724        assert_eq!(v.to_filter_value(), FilterValue::Null);
2725    }
2726
2727    #[test]
2728    fn to_filter_value_uuid_is_string() {
2729        let id = uuid::Uuid::nil();
2730        assert_eq!(id.to_filter_value(), FilterValue::String(id.to_string()));
2731    }
2732
2733    #[test]
2734    fn to_filter_value_bool_is_bool() {
2735        assert_eq!(true.to_filter_value(), FilterValue::Bool(true));
2736    }
2737}