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