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