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