Skip to main content

drizzle_core/expr/
cmp.rs

1//! Type-safe comparison functions.
2//!
3//! This module provides both function-based and method-based comparisons:
4//!
5//! ```rust
6//! # let _ = r####"
7//! // Function style
8//! eq(users.id, 42)
9//! gt(users.age, 18)
10//!
11//! // Method style (on SQLExpr)
12//! users.id.eq(42)
13//! users.age.gt(18)
14//! # "####;
15//! ```
16//!
17//! # Type Safety
18//!
19//! - `eq`, `neq`, `gt`, `gte`, `lt`, `lte`: Require compatible types
20//! - `like`, `not_like`: Require textual types on both sides
21//! - `between`: Requires expr compatible with both bounds
22//! - `is_null`, `is_not_null`: No type constraint (any type can be null-checked)
23
24use crate::dialect::DialectTypes;
25use crate::sql::{SQL, Token};
26use crate::traits::SQLParam;
27use crate::types::{Compatible, DataType, Textual};
28
29use super::{AggOr, AggregateKind, Expr, NonNull, SQLExpr};
30
31// =============================================================================
32// Internal Helper
33// =============================================================================
34
35fn binary_op<'a, V, L, R>(left: L, operator: Token, right: R) -> SQL<'a, V>
36where
37    V: SQLParam + 'a,
38    L: Expr<'a, V>,
39    R: ComparisonOperand<'a, V, L>,
40    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
41{
42    let left_sql = operand_sql(left);
43    let right_sql = ComparisonOperand::into_comparison_sql(right);
44
45    left_sql.push(operator).append(right_sql)
46}
47
48#[inline]
49fn operand_sql<'a, V, T>(value: T) -> SQL<'a, V>
50where
51    V: SQLParam + 'a,
52    T: Expr<'a, V>,
53{
54    value.into_expr_sql()
55}
56
57/// Type-safe operand for comparison functions.
58///
59/// This trait exists as an indirection layer between comparison functions
60/// (`eq`, `gt`, `like`, etc.) and the `Expr` trait. Rather than accepting
61/// any `Expr` directly, comparisons require `ComparisonOperand<'a, V, L>`
62/// where `L` is the left-hand expression type.
63///
64/// The blanket impl below only fires when `L::SQLType: Compatible<R::SQLType>`.
65/// Table macros can also implement this trait for custom column value types and
66/// a specific generated column ZST, which enables `eq(table.custom, value)`
67/// without making the value type a global SQL expression.
68pub trait ComparisonOperand<'a, V, L>: Sized
69where
70    V: SQLParam + 'a,
71    L: Expr<'a, V>,
72{
73    type SQLType: DataType;
74    type Aggregate: AggregateKind;
75
76    fn into_comparison_sql(self) -> SQL<'a, V>;
77}
78
79impl<'a, V, L, R> ComparisonOperand<'a, V, L> for R
80where
81    V: SQLParam + 'a,
82    L: Expr<'a, V>,
83    R: Expr<'a, V>,
84    L::SQLType: Compatible<R::SQLType>,
85{
86    type SQLType = R::SQLType;
87    type Aggregate = R::Aggregate;
88
89    fn into_comparison_sql(self) -> SQL<'a, V> {
90        self.into_expr_sql()
91    }
92}
93
94// =============================================================================
95// Equality Comparisons
96// =============================================================================
97
98/// Equality comparison (`=`).
99///
100/// Requires both operands to have compatible SQL types.
101///
102/// # Type Safety
103///
104/// ```rust
105/// # let _ = r####"
106/// // ✅ OK: Int compared with i32
107/// eq(users.id, 10);
108///
109/// // ✅ OK: Int compared with BigInt (integer family)
110/// eq(users.id, users.big_id);
111///
112/// // ❌ Compile error: Int cannot be compared with Text
113/// eq(users.id, "hello");
114/// # "####;
115/// ```
116#[allow(clippy::type_complexity)]
117pub fn eq<'a, V, L, R>(
118    left: L,
119    right: R,
120) -> SQLExpr<
121    'a,
122    V,
123    <V::DialectMarker as DialectTypes>::Bool,
124    NonNull,
125    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
126>
127where
128    V: SQLParam + 'a,
129    L: Expr<'a, V>,
130    R: ComparisonOperand<'a, V, L>,
131    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
132    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
133{
134    SQLExpr::new(binary_op(left, Token::EQ, right))
135}
136
137/// Inequality comparison (`<>` or `!=`).
138///
139/// Requires both operands to have compatible SQL types.
140#[allow(clippy::type_complexity)]
141pub fn neq<'a, V, L, R>(
142    left: L,
143    right: R,
144) -> SQLExpr<
145    'a,
146    V,
147    <V::DialectMarker as DialectTypes>::Bool,
148    NonNull,
149    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
150>
151where
152    V: SQLParam + 'a,
153    L: Expr<'a, V>,
154    R: ComparisonOperand<'a, V, L>,
155    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
156    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
157{
158    SQLExpr::new(binary_op(left, Token::NE, right))
159}
160
161// =============================================================================
162// Ordering Comparisons
163// =============================================================================
164
165/// Greater-than comparison (`>`).
166///
167/// Requires both operands to have compatible SQL types.
168#[allow(clippy::type_complexity)]
169pub fn gt<'a, V, L, R>(
170    left: L,
171    right: R,
172) -> SQLExpr<
173    'a,
174    V,
175    <V::DialectMarker as DialectTypes>::Bool,
176    NonNull,
177    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
178>
179where
180    V: SQLParam + 'a,
181    L: Expr<'a, V>,
182    R: ComparisonOperand<'a, V, L>,
183    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
184    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
185{
186    SQLExpr::new(binary_op(left, Token::GT, right))
187}
188
189/// Greater-than-or-equal comparison (`>=`).
190///
191/// Requires both operands to have compatible SQL types.
192#[allow(clippy::type_complexity)]
193pub fn gte<'a, V, L, R>(
194    left: L,
195    right: R,
196) -> SQLExpr<
197    'a,
198    V,
199    <V::DialectMarker as DialectTypes>::Bool,
200    NonNull,
201    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
202>
203where
204    V: SQLParam + 'a,
205    L: Expr<'a, V>,
206    R: ComparisonOperand<'a, V, L>,
207    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
208    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
209{
210    SQLExpr::new(binary_op(left, Token::GE, right))
211}
212
213/// Less-than comparison (`<`).
214///
215/// Requires both operands to have compatible SQL types.
216#[allow(clippy::type_complexity)]
217pub fn lt<'a, V, L, R>(
218    left: L,
219    right: R,
220) -> SQLExpr<
221    'a,
222    V,
223    <V::DialectMarker as DialectTypes>::Bool,
224    NonNull,
225    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
226>
227where
228    V: SQLParam + 'a,
229    L: Expr<'a, V>,
230    R: ComparisonOperand<'a, V, L>,
231    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
232    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
233{
234    SQLExpr::new(binary_op(left, Token::LT, right))
235}
236
237/// Less-than-or-equal comparison (`<=`).
238///
239/// Requires both operands to have compatible SQL types.
240#[allow(clippy::type_complexity)]
241pub fn lte<'a, V, L, R>(
242    left: L,
243    right: R,
244) -> SQLExpr<
245    'a,
246    V,
247    <V::DialectMarker as DialectTypes>::Bool,
248    NonNull,
249    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
250>
251where
252    V: SQLParam + 'a,
253    L: Expr<'a, V>,
254    R: ComparisonOperand<'a, V, L>,
255    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
256    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
257{
258    SQLExpr::new(binary_op(left, Token::LE, right))
259}
260
261// =============================================================================
262// Pattern Matching
263// =============================================================================
264
265/// LIKE pattern matching.
266///
267/// Requires both operands to be textual types (TEXT, VARCHAR).
268///
269/// # Type Safety
270///
271/// ```rust
272/// # let _ = r####"
273/// // ✅ OK: Text column with text pattern
274/// like(users.name, "%Alice%");
275///
276/// // ❌ Compile error: Int is not Textual
277/// like(users.id, "%123%");
278/// # "####;
279/// ```
280#[allow(clippy::type_complexity)]
281pub fn like<'a, V, L, R>(
282    left: L,
283    pattern: R,
284) -> SQLExpr<
285    'a,
286    V,
287    <V::DialectMarker as DialectTypes>::Bool,
288    NonNull,
289    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
290>
291where
292    V: SQLParam + 'a,
293    L: Expr<'a, V>,
294    R: ComparisonOperand<'a, V, L>,
295    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
296    L::SQLType: Textual,
297    <R as ComparisonOperand<'a, V, L>>::SQLType: Textual,
298    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
299{
300    SQLExpr::new(
301        operand_sql(left)
302            .push(Token::LIKE)
303            .append(ComparisonOperand::into_comparison_sql(pattern)),
304    )
305}
306
307/// NOT LIKE pattern matching.
308///
309/// Requires both operands to be textual types (TEXT, VARCHAR).
310#[allow(clippy::type_complexity)]
311pub fn not_like<'a, V, L, R>(
312    left: L,
313    pattern: R,
314) -> SQLExpr<
315    'a,
316    V,
317    <V::DialectMarker as DialectTypes>::Bool,
318    NonNull,
319    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
320>
321where
322    V: SQLParam + 'a,
323    L: Expr<'a, V>,
324    R: ComparisonOperand<'a, V, L>,
325    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
326    L::SQLType: Textual,
327    <R as ComparisonOperand<'a, V, L>>::SQLType: Textual,
328    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
329{
330    SQLExpr::new(
331        operand_sql(left)
332            .push(Token::NOT)
333            .push(Token::LIKE)
334            .append(ComparisonOperand::into_comparison_sql(pattern)),
335    )
336}
337
338// =============================================================================
339// Range Comparisons
340// =============================================================================
341
342/// BETWEEN comparison.
343///
344/// Checks if expr is between low and high (inclusive).
345/// Requires expr type to be compatible with both bounds.
346#[allow(clippy::type_complexity)]
347pub fn between<'a, V, E, L, H>(
348    expr: E,
349    low: L,
350    high: H,
351) -> SQLExpr<
352    'a,
353    V,
354    <V::DialectMarker as DialectTypes>::Bool,
355    NonNull,
356    <<E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E>>::Aggregate>>::Output as AggOr<
357        <H as ComparisonOperand<'a, V, E>>::Aggregate,
358    >>::Output,
359>
360where
361    V: SQLParam + 'a,
362    E: Expr<'a, V>,
363    L: ComparisonOperand<'a, V, E>,
364    H: ComparisonOperand<'a, V, E>,
365    E::SQLType: Compatible<<L as ComparisonOperand<'a, V, E>>::SQLType>,
366    E::SQLType: Compatible<<H as ComparisonOperand<'a, V, E>>::SQLType>,
367    E::Aggregate: AggOr<<L as ComparisonOperand<'a, V, E>>::Aggregate>,
368    <E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E>>::Aggregate>>::Output:
369        AggOr<<H as ComparisonOperand<'a, V, E>>::Aggregate>,
370{
371    SQLExpr::new(
372        SQL::from(Token::LPAREN)
373            .append(operand_sql(expr))
374            .push(Token::BETWEEN)
375            .append(ComparisonOperand::into_comparison_sql(low))
376            .push(Token::AND)
377            .append(ComparisonOperand::into_comparison_sql(high))
378            .push(Token::RPAREN),
379    )
380}
381
382/// NOT BETWEEN comparison.
383///
384/// Requires expr type to be compatible with both bounds.
385#[allow(clippy::type_complexity)]
386pub fn not_between<'a, V, E, L, H>(
387    expr: E,
388    low: L,
389    high: H,
390) -> SQLExpr<
391    'a,
392    V,
393    <V::DialectMarker as DialectTypes>::Bool,
394    NonNull,
395    <<E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E>>::Aggregate>>::Output as AggOr<
396        <H as ComparisonOperand<'a, V, E>>::Aggregate,
397    >>::Output,
398>
399where
400    V: SQLParam + 'a,
401    E: Expr<'a, V>,
402    L: ComparisonOperand<'a, V, E>,
403    H: ComparisonOperand<'a, V, E>,
404    E::SQLType: Compatible<<L as ComparisonOperand<'a, V, E>>::SQLType>,
405    E::SQLType: Compatible<<H as ComparisonOperand<'a, V, E>>::SQLType>,
406    E::Aggregate: AggOr<<L as ComparisonOperand<'a, V, E>>::Aggregate>,
407    <E::Aggregate as AggOr<<L as ComparisonOperand<'a, V, E>>::Aggregate>>::Output:
408        AggOr<<H as ComparisonOperand<'a, V, E>>::Aggregate>,
409{
410    SQLExpr::new(
411        SQL::from(Token::LPAREN)
412            .append(operand_sql(expr))
413            .push(Token::NOT)
414            .push(Token::BETWEEN)
415            .append(ComparisonOperand::into_comparison_sql(low))
416            .push(Token::AND)
417            .append(ComparisonOperand::into_comparison_sql(high))
418            .push(Token::RPAREN),
419    )
420}
421
422// =============================================================================
423// NULL Checks
424// =============================================================================
425
426/// IS NULL check.
427///
428/// Returns a boolean expression checking if the value is NULL.
429/// Any expression type can be null-checked.
430pub fn is_null<'a, V, E>(
431    expr: E,
432) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
433where
434    V: SQLParam + 'a,
435    E: Expr<'a, V>,
436{
437    SQLExpr::new(operand_sql(expr).push(Token::IS).push(Token::NULL))
438}
439
440/// IS NOT NULL check.
441///
442/// Returns a boolean expression checking if the value is not NULL.
443/// Any expression type can be null-checked.
444pub fn is_not_null<'a, V, E>(
445    expr: E,
446) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
447where
448    V: SQLParam + 'a,
449    E: Expr<'a, V>,
450{
451    SQLExpr::new(
452        operand_sql(expr)
453            .push(Token::IS)
454            .push(Token::NOT)
455            .push(Token::NULL),
456    )
457}
458
459// =============================================================================
460// IS DISTINCT FROM
461// =============================================================================
462
463/// IS DISTINCT FROM - NULL-safe inequality comparison.
464///
465/// Unlike `<>`, this treats NULL as a comparable value:
466/// - `NULL IS DISTINCT FROM NULL` → false
467/// - `NULL IS DISTINCT FROM 5` → true
468/// - `5 IS DISTINCT FROM NULL` → true
469///
470/// Supported by both `SQLite` (3.39+) and `PostgreSQL`.
471#[allow(clippy::type_complexity)]
472pub fn is_distinct_from<'a, V, L, R>(
473    left: L,
474    right: R,
475) -> SQLExpr<
476    'a,
477    V,
478    <V::DialectMarker as DialectTypes>::Bool,
479    NonNull,
480    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
481>
482where
483    V: SQLParam + 'a,
484    L: Expr<'a, V>,
485    R: ComparisonOperand<'a, V, L>,
486    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
487    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
488{
489    SQLExpr::new(
490        operand_sql(left)
491            .push(Token::IS)
492            .push(Token::DISTINCT)
493            .push(Token::FROM)
494            .append(ComparisonOperand::into_comparison_sql(right)),
495    )
496}
497
498/// IS NOT DISTINCT FROM - NULL-safe equality comparison.
499///
500/// Unlike `=`, this treats NULL as a comparable value:
501/// - `NULL IS NOT DISTINCT FROM NULL` → true
502/// - `NULL IS NOT DISTINCT FROM 5` → false
503///
504/// Supported by both `SQLite` (3.39+) and `PostgreSQL`.
505#[allow(clippy::type_complexity)]
506pub fn is_not_distinct_from<'a, V, L, R>(
507    left: L,
508    right: R,
509) -> SQLExpr<
510    'a,
511    V,
512    <V::DialectMarker as DialectTypes>::Bool,
513    NonNull,
514    <L::Aggregate as AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>>::Output,
515>
516where
517    V: SQLParam + 'a,
518    L: Expr<'a, V>,
519    R: ComparisonOperand<'a, V, L>,
520    L::SQLType: Compatible<<R as ComparisonOperand<'a, V, L>>::SQLType>,
521    L::Aggregate: AggOr<<R as ComparisonOperand<'a, V, L>>::Aggregate>,
522{
523    SQLExpr::new(
524        operand_sql(left)
525            .push(Token::IS)
526            .push(Token::NOT)
527            .push(Token::DISTINCT)
528            .push(Token::FROM)
529            .append(ComparisonOperand::into_comparison_sql(right)),
530    )
531}
532
533// =============================================================================
534// Boolean Testing
535// =============================================================================
536
537/// IS TRUE - tests if a boolean expression is true.
538///
539/// Unlike `= TRUE`, this handles NULL correctly:
540/// - `TRUE IS TRUE` → true
541/// - `FALSE IS TRUE` → false
542/// - `NULL IS TRUE` → false (not NULL!)
543pub fn is_true<'a, V, E>(
544    expr: E,
545) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
546where
547    V: SQLParam + 'a,
548    E: Expr<'a, V>,
549{
550    SQLExpr::new(operand_sql(expr).push(Token::IS).append(SQL::raw("TRUE")))
551}
552
553/// IS FALSE - tests if a boolean expression is false.
554///
555/// Unlike `= FALSE`, this handles NULL correctly:
556/// - `FALSE IS FALSE` → true
557/// - `TRUE IS FALSE` → false
558/// - `NULL IS FALSE` → false (not NULL!)
559pub fn is_false<'a, V, E>(
560    expr: E,
561) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, E::Aggregate>
562where
563    V: SQLParam + 'a,
564    E: Expr<'a, V>,
565{
566    SQLExpr::new(operand_sql(expr).push(Token::IS).append(SQL::raw("FALSE")))
567}
568
569// =============================================================================
570// Method-based Comparison API (Extension Trait)
571// =============================================================================
572
573/// Extension trait providing method-based comparisons for any `Expr` type.
574///
575/// This trait is blanket-implemented for all types implementing `Expr`,
576/// allowing method syntax on columns, literals, and expressions:
577///
578/// ```rust
579/// # let _ = r####"
580/// // Works on columns directly
581/// users.id.eq(42)
582/// users.age.gt(18)
583///
584/// // Chain with operators
585/// users.id.eq(42) & users.age.gt(18)
586/// # "####;
587/// ```
588pub trait ExprExt<'a, V: SQLParam>: Expr<'a, V> + Sized {
589    /// Equality comparison (`=`).
590    ///
591    /// ```rust
592    /// # let _ = r####"
593    /// users.id.eq(42)  // "users"."id" = 42
594    /// # "####;
595    /// ```
596    #[allow(clippy::type_complexity)]
597    fn eq<R>(
598        self,
599        other: R,
600    ) -> SQLExpr<
601        'a,
602        V,
603        <V::DialectMarker as DialectTypes>::Bool,
604        NonNull,
605        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
606    >
607    where
608        R: ComparisonOperand<'a, V, Self>,
609        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
610        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
611    {
612        eq(self, other)
613    }
614
615    /// Inequality comparison (`<>`).
616    ///
617    /// ```rust
618    /// # let _ = r####"
619    /// users.id.ne(42)  // "users"."id" <> 42
620    /// # "####;
621    /// ```
622    #[allow(clippy::type_complexity)]
623    fn ne<R>(
624        self,
625        other: R,
626    ) -> SQLExpr<
627        'a,
628        V,
629        <V::DialectMarker as DialectTypes>::Bool,
630        NonNull,
631        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
632    >
633    where
634        R: ComparisonOperand<'a, V, Self>,
635        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
636        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
637    {
638        neq(self, other)
639    }
640
641    /// Greater-than comparison (`>`).
642    ///
643    /// ```rust
644    /// # let _ = r####"
645    /// users.age.gt(18)  // "users"."age" > 18
646    /// # "####;
647    /// ```
648    #[allow(clippy::type_complexity)]
649    fn gt<R>(
650        self,
651        other: R,
652    ) -> SQLExpr<
653        'a,
654        V,
655        <V::DialectMarker as DialectTypes>::Bool,
656        NonNull,
657        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
658    >
659    where
660        R: ComparisonOperand<'a, V, Self>,
661        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
662        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
663    {
664        gt(self, other)
665    }
666
667    /// Greater-than-or-equal comparison (`>=`).
668    ///
669    /// ```rust
670    /// # let _ = r####"
671    /// users.age.ge(18)  // "users"."age" >= 18
672    /// # "####;
673    /// ```
674    #[allow(clippy::type_complexity)]
675    fn ge<R>(
676        self,
677        other: R,
678    ) -> SQLExpr<
679        'a,
680        V,
681        <V::DialectMarker as DialectTypes>::Bool,
682        NonNull,
683        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
684    >
685    where
686        R: ComparisonOperand<'a, V, Self>,
687        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
688        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
689    {
690        gte(self, other)
691    }
692
693    /// Less-than comparison (`<`).
694    ///
695    /// ```rust
696    /// # let _ = r####"
697    /// users.age.lt(65)  // "users"."age" < 65
698    /// # "####;
699    /// ```
700    #[allow(clippy::type_complexity)]
701    fn lt<R>(
702        self,
703        other: R,
704    ) -> SQLExpr<
705        'a,
706        V,
707        <V::DialectMarker as DialectTypes>::Bool,
708        NonNull,
709        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
710    >
711    where
712        R: ComparisonOperand<'a, V, Self>,
713        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
714        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
715    {
716        lt(self, other)
717    }
718
719    /// Less-than-or-equal comparison (`<=`).
720    ///
721    /// ```rust
722    /// # let _ = r####"
723    /// users.age.le(65)  // "users"."age" <= 65
724    /// # "####;
725    /// ```
726    #[allow(clippy::type_complexity)]
727    fn le<R>(
728        self,
729        other: R,
730    ) -> SQLExpr<
731        'a,
732        V,
733        <V::DialectMarker as DialectTypes>::Bool,
734        NonNull,
735        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
736    >
737    where
738        R: ComparisonOperand<'a, V, Self>,
739        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
740        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
741    {
742        lte(self, other)
743    }
744
745    /// LIKE pattern matching.
746    ///
747    /// ```rust
748    /// # let _ = r####"
749    /// users.name.like("%Alice%")  // "users"."name" LIKE '%Alice%'
750    /// # "####;
751    /// ```
752    #[allow(clippy::type_complexity)]
753    fn like<R>(
754        self,
755        pattern: R,
756    ) -> SQLExpr<
757        'a,
758        V,
759        <V::DialectMarker as DialectTypes>::Bool,
760        NonNull,
761        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
762    >
763    where
764        R: ComparisonOperand<'a, V, Self>,
765        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
766        Self::SQLType: Textual,
767        <R as ComparisonOperand<'a, V, Self>>::SQLType: Textual,
768        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
769    {
770        like(self, pattern)
771    }
772
773    /// NOT LIKE pattern matching.
774    ///
775    /// ```rust
776    /// # let _ = r####"
777    /// users.name.not_like("%Bot%")  // "users"."name" NOT LIKE '%Bot%'
778    /// # "####;
779    /// ```
780    #[allow(clippy::type_complexity)]
781    fn not_like<R>(
782        self,
783        pattern: R,
784    ) -> SQLExpr<
785        'a,
786        V,
787        <V::DialectMarker as DialectTypes>::Bool,
788        NonNull,
789        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
790    >
791    where
792        R: ComparisonOperand<'a, V, Self>,
793        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
794        Self::SQLType: Textual,
795        <R as ComparisonOperand<'a, V, Self>>::SQLType: Textual,
796        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
797    {
798        not_like(self, pattern)
799    }
800
801    /// IS NULL check.
802    ///
803    /// ```rust
804    /// # let _ = r####"
805    /// users.deleted_at.is_null()  // "users"."deleted_at" IS NULL
806    /// # "####;
807    /// ```
808    #[allow(clippy::wrong_self_convention)]
809    fn is_null(
810        self,
811    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
812        is_null(self)
813    }
814
815    /// IS NOT NULL check.
816    ///
817    /// ```rust
818    /// # let _ = r####"
819    /// users.email.is_not_null()  // "users"."email" IS NOT NULL
820    /// # "####;
821    /// ```
822    #[allow(clippy::wrong_self_convention)]
823    fn is_not_null(
824        self,
825    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
826        is_not_null(self)
827    }
828
829    /// BETWEEN comparison.
830    ///
831    /// Checks if the value is between low and high (inclusive).
832    ///
833    /// ```rust
834    /// # let _ = r####"
835    /// users.age.between(18, 65)  // ("users"."age" BETWEEN 18 AND 65)
836    /// # "####;
837    /// ```
838    #[allow(clippy::type_complexity)]
839    fn between<L, H>(
840        self,
841        low: L,
842        high: H,
843    ) -> SQLExpr<
844        'a,
845        V,
846        <V::DialectMarker as DialectTypes>::Bool,
847        NonNull,
848        <<Self::Aggregate as AggOr<
849            <L as ComparisonOperand<'a, V, Self>>::Aggregate,
850        >>::Output as AggOr<
851            <H as ComparisonOperand<'a, V, Self>>::Aggregate,
852        >>::Output,
853    >
854    where
855        L: ComparisonOperand<'a, V, Self>,
856        H: ComparisonOperand<'a, V, Self>,
857        Self::SQLType: Compatible<<L as ComparisonOperand<'a, V, Self>>::SQLType>,
858        Self::SQLType: Compatible<<H as ComparisonOperand<'a, V, Self>>::SQLType>,
859        Self::Aggregate:
860            AggOr<<L as ComparisonOperand<'a, V, Self>>::Aggregate>,
861        <Self::Aggregate as AggOr<
862            <L as ComparisonOperand<'a, V, Self>>::Aggregate,
863        >>::Output:
864            AggOr<<H as ComparisonOperand<'a, V, Self>>::Aggregate>,
865    {
866        between(self, low, high)
867    }
868
869    /// NOT BETWEEN comparison.
870    ///
871    /// Checks if the value is NOT between low and high.
872    ///
873    /// ```rust
874    /// # let _ = r####"
875    /// users.age.not_between(0, 17)  // ("users"."age" NOT BETWEEN 0 AND 17)
876    /// # "####;
877    /// ```
878    #[allow(clippy::type_complexity)]
879    fn not_between<L, H>(
880        self,
881        low: L,
882        high: H,
883    ) -> SQLExpr<
884        'a,
885        V,
886        <V::DialectMarker as DialectTypes>::Bool,
887        NonNull,
888        <<Self::Aggregate as AggOr<
889            <L as ComparisonOperand<'a, V, Self>>::Aggregate,
890        >>::Output as AggOr<
891            <H as ComparisonOperand<'a, V, Self>>::Aggregate,
892        >>::Output,
893    >
894    where
895        L: ComparisonOperand<'a, V, Self>,
896        H: ComparisonOperand<'a, V, Self>,
897        Self::SQLType: Compatible<<L as ComparisonOperand<'a, V, Self>>::SQLType>,
898        Self::SQLType: Compatible<<H as ComparisonOperand<'a, V, Self>>::SQLType>,
899        Self::Aggregate:
900            AggOr<<L as ComparisonOperand<'a, V, Self>>::Aggregate>,
901        <Self::Aggregate as AggOr<
902            <L as ComparisonOperand<'a, V, Self>>::Aggregate,
903        >>::Output:
904            AggOr<<H as ComparisonOperand<'a, V, Self>>::Aggregate>,
905    {
906        not_between(self, low, high)
907    }
908
909    /// IN array check.
910    ///
911    /// Checks if the value is in the provided array.
912    ///
913    /// ```rust
914    /// # let _ = r####"
915    /// users.role.in_array([Role::Admin, Role::Moderator])
916    /// // "users"."role" IN ('admin', 'moderator')
917    /// # "####;
918    /// ```
919    fn in_array<I, R>(
920        self,
921        values: I,
922    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
923    where
924        I: IntoIterator<Item = R>,
925        R: Expr<'a, V>,
926        Self::SQLType: Compatible<R::SQLType>,
927    {
928        crate::expr::in_array(self, values)
929    }
930
931    /// NOT IN array check.
932    ///
933    /// Checks if the value is NOT in the provided array.
934    ///
935    /// ```rust
936    /// # let _ = r####"
937    /// users.role.not_in_array([Role::Banned, Role::Suspended])
938    /// // "users"."role" NOT IN ('banned', 'suspended')
939    /// # "####;
940    /// ```
941    fn not_in_array<I, R>(
942        self,
943        values: I,
944    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
945    where
946        I: IntoIterator<Item = R>,
947        R: Expr<'a, V>,
948        Self::SQLType: Compatible<R::SQLType>,
949    {
950        crate::expr::not_in_array(self, values)
951    }
952
953    /// IN subquery check.
954    fn in_subquery<S>(
955        self,
956        subquery: S,
957    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
958    where
959        S: Expr<'a, V>,
960        Self::SQLType: Compatible<S::SQLType>,
961    {
962        crate::expr::in_subquery(self, subquery)
963    }
964
965    /// NOT IN subquery check.
966    fn not_in_subquery<S>(
967        self,
968        subquery: S,
969    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate>
970    where
971        S: Expr<'a, V>,
972        Self::SQLType: Compatible<S::SQLType>,
973    {
974        crate::expr::not_in_subquery(self, subquery)
975    }
976
977    /// IS DISTINCT FROM - NULL-safe inequality comparison.
978    ///
979    /// ```rust
980    /// # let _ = r####"
981    /// users.status.is_distinct_from("active")
982    /// // "users"."status" IS DISTINCT FROM 'active'
983    /// # "####;
984    /// ```
985    #[allow(clippy::type_complexity, clippy::wrong_self_convention)]
986    fn is_distinct_from<R>(
987        self,
988        other: R,
989    ) -> SQLExpr<
990        'a,
991        V,
992        <V::DialectMarker as DialectTypes>::Bool,
993        NonNull,
994        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
995    >
996    where
997        R: ComparisonOperand<'a, V, Self>,
998        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
999        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
1000    {
1001        is_distinct_from(self, other)
1002    }
1003
1004    /// IS NOT DISTINCT FROM - NULL-safe equality comparison.
1005    ///
1006    /// ```rust
1007    /// # let _ = r####"
1008    /// users.status.is_not_distinct_from("active")
1009    /// // "users"."status" IS NOT DISTINCT FROM 'active'
1010    /// # "####;
1011    /// ```
1012    #[allow(clippy::type_complexity, clippy::wrong_self_convention)]
1013    fn is_not_distinct_from<R>(
1014        self,
1015        other: R,
1016    ) -> SQLExpr<
1017        'a,
1018        V,
1019        <V::DialectMarker as DialectTypes>::Bool,
1020        NonNull,
1021        <Self::Aggregate as AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>>::Output,
1022    >
1023    where
1024        R: ComparisonOperand<'a, V, Self>,
1025        Self::SQLType: Compatible<<R as ComparisonOperand<'a, V, Self>>::SQLType>,
1026        Self::Aggregate: AggOr<<R as ComparisonOperand<'a, V, Self>>::Aggregate>,
1027    {
1028        is_not_distinct_from(self, other)
1029    }
1030
1031    /// IS TRUE - boolean test that handles NULL.
1032    ///
1033    /// ```rust
1034    /// # let _ = r####"
1035    /// users.is_active.is_true()
1036    /// // "users"."is_active" IS TRUE
1037    /// # "####;
1038    /// ```
1039    #[allow(clippy::wrong_self_convention)]
1040    fn is_true(
1041        self,
1042    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
1043        is_true(self)
1044    }
1045
1046    /// IS FALSE - boolean test that handles NULL.
1047    ///
1048    /// ```rust
1049    /// # let _ = r####"
1050    /// users.is_active.is_false()
1051    /// // "users"."is_active" IS FALSE
1052    /// # "####;
1053    /// ```
1054    #[allow(clippy::wrong_self_convention)]
1055    fn is_false(
1056        self,
1057    ) -> SQLExpr<'a, V, <V::DialectMarker as DialectTypes>::Bool, NonNull, Self::Aggregate> {
1058        is_false(self)
1059    }
1060}
1061
1062/// Blanket implementation for all `Expr` types.
1063impl<'a, V: SQLParam, E: Expr<'a, V>> ExprExt<'a, V> for E {}