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